tamper-signal 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md ADDED
@@ -0,0 +1,235 @@
1
+ # Integrating Tamper Signal: a runbook for coding agents
2
+
3
+ You are reading this because a user pointed you at this repo and said something
4
+ like "add tamper signal," "add the signal to my dashboard," or "give my data
5
+ receipts." This file is the complete integration path. Follow it in order;
6
+ every step is verifiable.
7
+
8
+ What you are installing: signed receipts for a data pipeline. Every pipeline
9
+ stage signs a receipt (a hash of its input, its code, and its output, plus
10
+ human-legible control totals). Receipts link into a chain; `receipts verify`
11
+ and an in-browser signal re-verify the whole chain and report a traffic light.
12
+ Green: intact. Yellow: verifies, with caveats a human should look at. Red:
13
+ broken, at an exact link, with the totals delta.
14
+
15
+ ## Hard rules for anything you write while integrating
16
+
17
+ These govern copy, code comments, commit messages, and UI text you produce:
18
+
19
+ 1. **This proves continuity, not correctness.** Never write that it "ensures
20
+ accuracy," "validates correctness," or "guarantees data quality." The
21
+ honest claim, verbatim when you need one: *"It can't tell you the data is
22
+ right, but it can prove nobody changed it."*
23
+ 2. The verdict lines are fixed (see `docs/MESSAGING.md`, the copy source of
24
+ truth): green "The light is green, the data is clean." / yellow "The light
25
+ is yellow, a human should look." / red "The light is red, the chain is
26
+ broken." Only green rhymes. Do not invent new rhymes.
27
+ 3. No em dashes in user-facing copy.
28
+ 4. Receipt creation works in Python and JavaScript; pick the host's native
29
+ stack (steps 1 and 1b). Do not promise features that are not in this
30
+ file.
31
+ 5. Never commit a private key. `keys/` and `*.key` belong in .gitignore.
32
+
33
+ ## 1. Install (Python pipelines)
34
+
35
+ Requires Python 3.11+.
36
+
37
+ ```bash
38
+ pip install git+https://github.com/welovejeff/tamper-evident-verification.git
39
+ ```
40
+
41
+ This provides the `receipts` CLI and the `tamper_signal` Python package.
42
+ Verify: `receipts --help` exits 0. JavaScript-only project? Use step 1b and
43
+ the JS equivalents; the two stacks produce interchangeable chains.
44
+
45
+ ## 1b. Install (JavaScript pipelines)
46
+
47
+ Requires Node 18.17+.
48
+
49
+ ```bash
50
+ npm install tamper-signal
51
+ ```
52
+
53
+ This provides the `tamper-signal` CLI (keygen / ingest / verify, exit codes
54
+ 0 green, 1 red, 2 yellow) and the programmatic API:
55
+
56
+ ```js
57
+ import { receiptStep, loadCsv } from "tamper-signal";
58
+
59
+ const clean = receiptStep(
60
+ (records) => records.filter((r) => r.campaign_name !== null),
61
+ { chainDir: "receipts/", keyPath: "keys/signing.key", codeFile: "pipeline.js" }
62
+ );
63
+ const output = await clean(loadCsv("export.csv"));
64
+ ```
65
+
66
+ `receiptStep` wraps a sync or async records -> records function with the
67
+ same contract as Python's `receipt_step`: verify the chain tail first,
68
+ refuse foreign input, sign and append a receipt. The browser files are the
69
+ same package: `tamper-signal/light`, `tamper-signal/badge`,
70
+ `tamper-signal/element`, `tamper-signal/react`. JS reads .csv/.tsv/.json/
71
+ .ndjson; only the Python side reads .xlsx.
72
+
73
+ ## 2. Generate a signing keypair (once per project)
74
+
75
+ ```bash
76
+ receipts keygen --out keys/
77
+ ```
78
+
79
+ Writes `keys/signing.key` (private, PEM; never commit) and `keys/signing.pub`
80
+ (raw 32-byte hex; safe to commit). Add `keys/` and `*.key` to .gitignore now.
81
+
82
+ ## 3. Start the chain at the source export
83
+
84
+ ```bash
85
+ receipts ingest path/to/export.xlsx --origin "TikTok export, May 2026" \
86
+ --key keys/signing.key --out receipts/
87
+ ```
88
+
89
+ `--origin` is free text describing where the file came from; it appears in the
90
+ signal's UI, so write it for humans. Input formats: .xlsx/.xlsm (Python only),
91
+ .csv, .tsv, .json (array of objects), .ndjson/.jsonl. The semantic hash is
92
+ identical across formats, so an xlsx ingest verifies against a CSV or JSON
93
+ copy of the same data. This writes `receipts/000_source.json` and
94
+ `receipts/chain.json`.
95
+
96
+ ## 4. Wrap every transform stage
97
+
98
+ ```python
99
+ from tamper_signal import receipt_step
100
+
101
+ @receipt_step(chain_dir="receipts/", key_path="keys/signing.key")
102
+ def transform_clean(records):
103
+ return [r for r in records if r.get("campaign_name")]
104
+ ```
105
+
106
+ Contract: the function takes and returns either a list of dicts or a pandas
107
+ DataFrame (frames are hashed as records and pass through the function
108
+ untouched; NaN becomes the canonical null). The wrapper verifies the existing
109
+ chain first, refuses to run if the input data does not descend from the chain
110
+ tail (`ChainTailMismatch`), then signs and appends a receipt for the stage.
111
+
112
+ If a stage cannot fit the list-of-dicts contract, leave it unwrapped and tell
113
+ the user that stage is not attested. Do not fabricate a receipt for work the
114
+ wrapper did not observe.
115
+
116
+ ## 5. Verify from the command line
117
+
118
+ ```bash
119
+ receipts verify receipts/chain.json --pub keys/signing.pub --data path/to/dashboard_data.xlsx
120
+ ```
121
+
122
+ Exit codes are the traffic light: **0 green, 1 red, 2 yellow**. `--data` is
123
+ optional and checks the file the dashboard actually reads against the final
124
+ receipt. In CI: fail the build on exit 1; surface exit 2 to a human rather
125
+ than failing silently. `--warn-drift` additionally flags any control-totals
126
+ movement across links (only for pipelines expected to preserve totals).
127
+
128
+ ## 6. Add the signal to the host UI
129
+
130
+ With a bundler, import straight from the npm package
131
+ (`import { mountTamperSignal } from "tamper-signal/light"`). Without one,
132
+ vendor two files from this repo into the host app, side by side (light.js
133
+ imports `./badge.js` relatively):
134
+
135
+ - `badge/badge.js` (verification core + the expandable badge)
136
+ - `badge/light.js` (the signal: the inline status light)
137
+
138
+ Serve the `receipts/` directory statically, then mount the signal in the host
139
+ header:
140
+
141
+ ```html
142
+ <script type="module">
143
+ import { mountTamperSignal } from "/static/light.js";
144
+ mountTamperSignal(document.querySelector("header"), "/receipts/chain.json");
145
+ </script>
146
+ ```
147
+
148
+ React hosts: `import { TamperSignal } from "tamper-signal/react"` (or vendor
149
+ `badge/light-react.js`), then `<TamperSignal chain="/receipts/chain.json" />`.
150
+
151
+ Any other framework, or plain HTML: the web component. Import
152
+ `tamper-signal/element` (or vendor `badge/element.js`, which needs light.js
153
+ and badge.js beside it) and write one tag:
154
+
155
+ ```html
156
+ <tamper-signal chain="/receipts/chain.json"></tamper-signal>
157
+ ```
158
+
159
+ Attributes mirror the options: `pub-key`, `watch`, `warn-drift`,
160
+ `receipts-href`, `theme`.
161
+
162
+ Static serving examples: Flask
163
+ `app = Flask(__name__, static_folder="receipts", static_url_path="/receipts")`;
164
+ FastAPI `app.mount("/receipts", StaticFiles(directory="receipts"))`; Express
165
+ `app.use("/receipts", express.static("receipts"))`. A purely static site can
166
+ copy `receipts/` into its public directory at build time.
167
+
168
+ Placement: the right end of the host header, after the host's own controls.
169
+ The pill is intentionally dark and mono; do not restyle it to match the host
170
+ theme (on a dark host, pass `{ theme: "light" }` instead). Options on the
171
+ fourth argument: `watch` (re-verify every N ms), `warnDrift`, `receiptsHref`,
172
+ `theme`.
173
+
174
+ The expandable badge (`renderReceiptBadge(el, "/receipts/chain.json")`) is the
175
+ alternative for pages with room for a full-width strip.
176
+
177
+ ## 7. Let the signal flag broken metrics
178
+
179
+ Add `data-receipt-column="<column>"` to any metric element whose value comes
180
+ from a chain column. When the chain breaks, the signal outlines the elements
181
+ whose columns moved at the broken link and tags them "tamper signal:
182
+ unverified value."
183
+
184
+ Column names must match the normalized names in the receipts' control totals
185
+ (lowercased, spaces to underscores). Do not guess: read
186
+ `receipts/chain.json`, open the listed receipt files, and use the exact keys
187
+ under `control_totals.numeric_sums` / `null_counts`.
188
+
189
+ ## 8. The Data tab (when asked for table UI or views)
190
+
191
+ The project's stance: a dashboard built on verified data should show the
192
+ verified table, not just charts. If the user asks for the table UI, add a
193
+ "Data" tab next to the charts that renders the final stage's output (the same
194
+ file `--data` verifies), labeled with the current verdict. The design
195
+ reference is `designs/03-data-tab.html` with notes in `designs/03-NOTES.md`;
196
+ there is no packaged component yet, so build it in the host's own stack and
197
+ say so honestly.
198
+
199
+ ## 9. Verify your work before reporting done
200
+
201
+ 1. `receipts verify receipts/chain.json --pub keys/signing.pub` exits 0.
202
+ 2. Load the host page: the pill reads `VERIFIED · chain intact` (click it for
203
+ the per-stage popover).
204
+ 3. Negative test without touching the user's real chain: this repo commits
205
+ known-good fixtures under `examples/chains/` (`intact/` verifies green;
206
+ `tampered/` is broken at link 1 -> 2). Point the signal at each to confirm
207
+ both states render.
208
+ 4. If the pill reads `UNVERIFIED · could not load chain`, the receipts
209
+ directory is not being served at the URL you passed (or it is blocked by
210
+ CORS). That state is a capability fallback, not a verdict.
211
+
212
+ ## Troubleshooting
213
+
214
+ - `ChainTailMismatch` when running a wrapped transform: the data fed to the
215
+ stage is not the previous stage's output. Re-run the pipeline from ingest;
216
+ do not bypass the wrapper.
217
+ - Yellow with "unrecognized signing key": the `pubKeyHex` passed to the signal
218
+ differs from the key embedded in chain.json. Pass the right trusted key, or
219
+ omit the argument to trust the embedded key.
220
+ - The pill shows `UNVERIFIED · verification unsupported in this browser`: the
221
+ browser lacks Web Crypto Ed25519. The signal says nothing about the chain in
222
+ this state; verify from the CLI instead.
223
+
224
+ ## Repo map
225
+
226
+ | Path | What it is |
227
+ |---|---|
228
+ | `tamper_signal/` | Python package: canonicalization, keys, receipts, verify, `receipt_step` |
229
+ | `node/` | JavaScript package: same API (`receiptStep`, `verifyChain`), interchangeable chains |
230
+ | `badge/badge.js` | Browser verification core + the receipt badge |
231
+ | `badge/light.js` | The signal (inline status light), `mountTamperSignal` |
232
+ | `badge/light-react.js` | `<TamperSignal />` React wrapper |
233
+ | `examples/chains/` | Committed known-good and known-broken demo chains |
234
+ | `designs/` | Working HTML mockups for the signal, console, and Data tab |
235
+ | `docs/MESSAGING.md` | Copy rules; the source of truth for any words you write |
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jeff MacDonald
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,169 @@
1
+ <sub>tamper-signal</sub>
2
+
3
+ # The light is green, the data is clean.
4
+
5
+ Your social team exports a month of TikTok performance data. Someone vibe-codes a dashboard on top of it with an AI assistant in an afternoon. It looks great. Then a transform silently drops 22 rows, or the model hallucinates an aggregation, and the numbers in front of your boss are wrong. Nothing in that workflow catches it. This is the missing verification layer: every stage of the pipeline signs a receipt for what went in and what came out, and one command (or a badge on the dashboard itself) tells you whether the chain is intact, or exactly where it broke and by how much.
6
+
7
+ **Live demo:** [welovejeff.github.io/tamper-evident-verification](https://welovejeff.github.io/tamper-evident-verification/) re-verifies a real committed receipt chain in your browser: swap in a tampered chain or an untrusted key and watch the light catch it.
8
+
9
+ **Pointing a coding agent at this repo?** `AGENTS.md` is the full integration runbook: install, keygen, ingest, wrap transforms, mount the signal, verify. Tell your agent "add tamper signal" and it will find it.
10
+
11
+ ## The problem
12
+
13
+ Vibe-coded pipelines fail silently. AI-generated transform scripts work most of the time, and when they don't, they don't crash. They drop rows. They double-count. They coerce a column wrong and quietly shift every total. The dashboard still renders. The chart still looks plausible. Nobody re-checks 48,000 rows by hand.
14
+
15
+ Traditional answers (warehouse lineage, dbt, data-quality suites) assume infrastructure a small team running xlsx-to-dashboard doesn't have. This is the lightweight version: signed receipts as files on disk, no database, no server, no catalog.
16
+
17
+ ## The traffic light
18
+
19
+ The badge and the verifier reduce the whole chain to one state:
20
+
21
+ - 🟢 **Green.** Every link in the receipt chain verifies. Every signature is valid. The data made it from the original export to the dashboard unchanged.
22
+ - 🟡 **Yellow.** Verifiable, but with caveats: gaps in receipt coverage, an unrecognized signing key, or control-total drift that needs a human look.
23
+ - 🔴 **Red.** Chain broken. A hash doesn't match at a specific link. You get the exact stage and the control-totals delta (e.g. `row_count 48212 -> 48190 (-22)`).
24
+
25
+ ![The inline status light cycling green, yellow, and red inside a host dashboard, then flagging the unverified metric](docs/media/light.gif)
26
+
27
+ *The inline status light: a small dark instrument in your dashboard's header. When the chain breaks, it reaches into the page and flags the exact metric that no longer descends from the source.*
28
+
29
+ Honest status: all three verdicts are implemented in `receipts verify` and the browser badge. Yellow today covers two detectable caveats (a coverage gap in the receipt numbering, and signatures that only verify under the chain's embedded key rather than the key you trust) plus opt-in control-total drift via `--warn-drift`. The Data tab and console animations in this README are design previews of later interface tiers, built from the working mockups in `designs/`. The badge also renders a separate amber state ("could not load" or "verification unsupported in this browser"); that is a capability fallback that says nothing about the chain, not the yellow verdict.
30
+
31
+ ## 60-second quickstart
32
+
33
+ Python 3.11+. Open source (MIT), `pip`-installable.
34
+
35
+ ```bash
36
+ git clone <this repo> && cd tamper-evident-verification
37
+ pip install -e .
38
+ receipts demo
39
+ ```
40
+
41
+ `receipts demo` runs the whole story end to end: generates a deliberately messy sample export, ingests it, runs two AI-written-style transforms, verifies the chain (PASS), then tampers with one spend value and verifies again (FAIL, pinpointing the broken link and the totals delta). It finishes by serving the badge at `http://localhost:8000/badge/badge.html` so you can see green, yellow, and red side by side.
42
+
43
+ ## CLI
44
+
45
+ ```bash
46
+ receipts keygen --out keys/
47
+ receipts ingest sample_export.xlsx --origin "TikTok export, May 2026" --key keys/signing.key --out receipts/
48
+ receipts verify receipts/chain.json --pub keys/signing.pub --data dashboard.xlsx
49
+ ```
50
+
51
+ `ingest` and `verify --data` accept .xlsx, .csv, .tsv, .json (array of objects), and .ndjson; the semantic hash is identical across formats, so an xlsx ingest verifies against a CSV copy of the same data. `verify` exits with the traffic light: 0 green, 1 red, 2 yellow (verifies, with caveats). Add `--warn-drift` to also flag any control-totals movement across links as a caveat; it is off by default because filters and aggregations legitimately move totals.
52
+
53
+ Transforms record their own receipts by wrapping any list-of-dicts to list-of-dicts function:
54
+
55
+ ```python
56
+ from tamper_signal import receipt_step
57
+
58
+ @receipt_step(chain_dir="receipts/", key_path="keys/signing.key")
59
+ def transform_clean(records):
60
+ return [r for r in records if r.get("campaign_name")]
61
+ ```
62
+
63
+ The wrapper verifies the chain tail first, refuses to run if the input hash doesn't match it, runs the function, then signs and appends a receipt. Transforms can also take and return pandas DataFrames; frames are hashed as records and pass through untouched.
64
+
65
+ ## JavaScript pipelines
66
+
67
+ The same receipts, native to Node (18.17+): `npm install tamper-signal` provides a `tamper-signal` CLI (keygen, ingest, verify, with the same exit codes) and a programmatic API. Chains are interchangeable across the two stacks; the canonicalization is byte-identical, proven by golden vectors generated from the Python side.
68
+
69
+ ```js
70
+ import { receiptStep, loadCsv } from "tamper-signal";
71
+
72
+ const clean = receiptStep(
73
+ (records) => records.filter((r) => r.campaign_name !== null),
74
+ { chainDir: "receipts/", keyPath: "keys/signing.key" }
75
+ );
76
+ const output = await clean(loadCsv("export.csv"));
77
+ ```
78
+
79
+ JavaScript reads .csv, .tsv, .json, and .ndjson; spreadsheets go through the Python CLI. The browser surfaces ship in the same package: `tamper-signal/light`, `tamper-signal/badge`, `tamper-signal/element`, `tamper-signal/react`.
80
+
81
+ ## How the chain works
82
+
83
+ ```
84
+ TikTok/Sprinklr export.xlsx
85
+ |
86
+ v
87
+ [ingest] ──────────> 000_source.json evidence hash + semantic hash + totals, signed
88
+ |
89
+ v
90
+ [transform_clean] ─> 001_transform_clean.json input hash == previous output hash
91
+ |
92
+ v
93
+ [transform_agg] ──> 002_transform_aggregate.json
94
+ |
95
+ v
96
+ dashboard data <─── receipts verify: walk every link, check every signature
97
+ ```
98
+
99
+ Each receipt contains the SHA-256 of its input, the SHA-256 of the transform's source code, the SHA-256 of its output, and human-legible control totals (row counts, numeric sums, date ranges, null counts). Receipts link because each stage's input hash must equal the prior stage's output hash. Everything is signed with Ed25519; `chain.json` is just an ordered list of receipt files plus the public key.
100
+
101
+ Two hashes exist per artifact. The **evidence hash** anchors the raw file bytes at ingest. The **semantic hash** covers the canonicalized data content, stable across format round-trips (xlsx re-save, xlsx to CSV, xlsx to JSON) so long as the values are unchanged. Row order is not part of integrity: rows are sorted before hashing.
102
+
103
+ When verification fails, you don't get a shrug. You get the link:
104
+
105
+ ```
106
+ ✗ CHAIN BROKEN at link 1 -> 2 (transform_aggregate)
107
+ expected input hash a3f1...9c (output of transform_clean)
108
+ found input hash 77b2...d4
109
+ Control totals delta vs upstream: row_count 48212 -> 48190 (-22), spend_(usd) -98.40
110
+ ```
111
+
112
+ Hashes say "broken." Totals say "how broken."
113
+
114
+ ## The badge
115
+
116
+ `badge/badge.js` exports `renderReceiptBadge(containerEl, chainUrl, pubKeyHex)`. Drop it into any web frontend, point it at your `receipts/chain.json`, and it re-verifies the whole chain client-side with Web Crypto Ed25519: every signature, every hash link. No build step, no framework, no server-side trust. The badge re-checks hash links only; it does not re-canonicalize xlsx in the browser.
117
+
118
+ ![Receipt badge: green intact chain and red broken chain](badge/badge-demo.png)
119
+
120
+ Green collapsed state reads like: `✓ Verified · TikTok export, May 2026 · 48,212 rows · 2 transforms · chain intact`. Expanding shows one row per receipt.
121
+
122
+ ## The signal: an inline status light
123
+
124
+ `badge/light.js` is the v1 dashboard UI: a small dark pill that mounts in your header, runs the same in-browser verification as the badge, and shows the verdict as the light. It deliberately refuses to adopt your dashboard's theme; like a tamper sticker, its value comes from being recognizable anywhere. One call:
125
+
126
+ ```html
127
+ <script type="module">
128
+ import { mountTamperSignal } from "/badge/light.js";
129
+ mountTamperSignal(document.querySelector("header"), "/receipts/chain.json");
130
+ </script>
131
+ ```
132
+
133
+ React, with a bundler: `import { TamperSignal } from "tamper-signal/react"` and `<TamperSignal chain="/receipts/chain.json" />`. Everything else (Vue, Svelte, plain HTML): import `tamper-signal/element` and write `<tamper-signal chain="/receipts/chain.json"></tamper-signal>`.
134
+
135
+ The pill expands to a popover: the per-stage table when green, the caveat list when yellow, the broken link with its totals delta when red. In the red state the light also reaches into the page: give any metric element a `data-receipt-column="spend_usd"` attribute, and if that column moved at the broken link the element gets outlined and tagged `tamper signal: unverified value`. Mark up your metrics once and the light flags the exact number that no longer descends from the source.
136
+
137
+ Options on the fourth argument: `watch` (re-verify every N ms and pulse on transitions), `warnDrift`, `receiptsHref`, and `theme: "light"` so the pill stays the one foreign object on a dark host. `receipts demo` serves a live three-state example at `http://localhost:8000/badge/light.html`.
138
+
139
+ ## Dashboards should show their work
140
+
141
+ We think any dashboard built on verified data should let you see the data. Not a tooltip, not an export-on-request: a Data tab, right next to the charts, showing the raw verified table the pretty numbers came from. If the chain is intact and the light is green, there is no reason to hide the rows, and if you find yourself wanting to hide them, that's worth sitting with. A chart asks you to believe; a table lets you check. Green light, open table: that's the whole standard.
142
+
143
+ ![The Data tab: the dashboard flips to a dark raw-table view where a broken chain is localized to the views column](docs/media/data-tab.gif)
144
+
145
+ *Design preview: install the verification layer and your dashboard grows a Data tab. When the chain breaks, the break is localized to the column and total that no longer verify, right in the table.*
146
+
147
+ ## What this proves, and what it doesn't
148
+
149
+ This proves **continuity, not correctness**. It can't tell you the data is right, but it can prove nobody changed it. The chain shows the dashboard numbers descend from the ingested export through a known sequence of code, and it locates the exact stage where a number changed unexpectedly. If the source export is itself wrong, the chain faithfully verifies wrong numbers. It is not a data-quality tool.
150
+
151
+ Also worth knowing: the signing key lives on your machine, and today that local Ed25519 keypair is the whole root of trust. Anyone holding the key can sign a fresh, internally consistent chain. External anchoring (below) is what closes that gap.
152
+
153
+ ## Roadmap
154
+
155
+ - **Richer yellow taxonomy.** Yellow currently detects coverage gaps, unrecognized signing keys, and opt-in totals drift. Distinct severities and smarter drift heuristics are open questions (see `designs/01-NOTES.md`).
156
+ - **External anchoring.** Sigstore transparency logs or RFC 3161 timestamps, so a chain can't be silently re-signed after the fact. The attachment points are already marked `FUTURE:` in `tamper_signal/keys.py` and `tamper_signal/receipts.py`.
157
+ - **Verification console.** A devtools-for-data window: the receipt chain as an inspectable pipeline, an event log of verify runs, and the break pinned at the severed link.
158
+
159
+ ![The verification console: a pipeline of signed receipts where a tampered stage severs the chain at the exact link](docs/media/console.gif)
160
+
161
+ *Design preview of the verification console: calm when green, surgical when red.*
162
+
163
+ ## Relation to OpenLineage, dbt, and Great Expectations
164
+
165
+ Those tools model lineage and quality at the warehouse and orchestration layer. This is narrower and lighter: a signed, file-based receipt chain you can drop in front of an ad-hoc, vibe-coded xlsx-to-dashboard pipeline without a database, a server, or a metadata catalog. A complement for the gap before those tools are in place, not a replacement.
166
+
167
+ ## Contributing
168
+
169
+ Open source under the MIT license (see `LICENSE`), designed to be added to any vibe-coded data project. The Python package is in `tamper_signal/`, tests in `tests/` (run `pytest`), examples in `examples/`, the badge in `badge/`. Issues and PRs welcome. The original Luhn hash demo lives unchanged in `legacy/` and is off the main path.