tamper-signal 2.0.0 → 2.1.1

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 CHANGED
@@ -569,7 +569,15 @@ mode 0400, never in the process environment) — do **not** put the key material
569
569
  in `EnvironmentFile`, which would expose it via `/proc/<pid>/environ`. Keep the
570
570
  key file `0600`; the watcher fails closed if it is group/world-readable.
571
571
 
572
- ## 6. Add the signal to the host UI
572
+ ## 6. Add the light to the host UI (the room is already served)
573
+
574
+ The UI is two things, always shipped together: **the light** (the inline
575
+ status pill, the lightest touch) and **the room** (the one robust surface
576
+ behind it — the attested data table with the chain rail, break exhibit,
577
+ receipt inspector, event log, chain of custody, and evidence export). The
578
+ attach helpers below serve the room automatically and pre-wire the light's
579
+ "view receipts →" link to it; if you mount by hand, wiring `receiptsHref` to
580
+ a room is YOUR one extra line — the light must never dead-end in raw JSON.
573
581
 
574
582
  With a bundler, import straight from the npm package
575
583
  (`import { mountTamperSignal } from "tamper-signal/light"`). Without one, copy
@@ -580,24 +588,34 @@ through `site-packages` or `node_modules`):
580
588
  tamper-signal assets --out badge/ # Python; tamper-signal assets --out badge/ on Node
581
589
  ```
582
590
 
583
- That writes `light.js`, `badge.js`, `element.js`, `table.js`, and `console.js`
584
- into `badge/`. For the inline signal you need two of them side by side (light.js
585
- imports `./badge.js` relatively):
591
+ That writes `light.js`, `badge.js`, `element.js`, `table.js`, `console.js`,
592
+ and `room.js` into `badge/`. For the inline signal you need two of them side
593
+ by side (light.js imports `./badge.js` relatively); the room needs `room.js`
594
+ and `badge.js`:
586
595
 
587
- - `badge/badge.js` (verification core + the expandable badge)
596
+ - `badge/badge.js` (the shared verification core)
588
597
  - `badge/light.js` (the signal: the inline status light)
598
+ - `badge/room.js` (the room: `mountSignalRoom`, `<tamper-signal-room>`)
589
599
 
590
600
  Serve the `receipts/` directory statically, then mount the signal in the host
591
- header. Import the asset from wherever you served it; the snippets here assume
592
- you vendored into `badge/` and serve it at `/badge/`:
601
+ header and a room for it to link to. Import the assets from wherever you
602
+ served them; the snippets here assume you vendored into `badge/` and serve it
603
+ at `/badge/`:
593
604
 
594
605
  ```html
595
606
  <script type="module">
596
607
  import { mountTamperSignal } from "/badge/light.js";
597
- mountTamperSignal(document.querySelector("header"), "/receipts/chain.json");
608
+ import { mountSignalRoom } from "/badge/room.js";
609
+ mountSignalRoom(document.querySelector("#data"), "/receipts/chain.json");
610
+ mountTamperSignal(document.querySelector("header"), "/receipts/chain.json",
611
+ undefined, { receiptsHref: "#tamper-room" });
598
612
  </script>
599
613
  ```
600
614
 
615
+ (The first room in a document owns the id `tamper-room`, so `#tamper-room`
616
+ always reaches it. If the room lives on its own page, point `receiptsHref` at
617
+ that URL instead — the attach helpers do exactly this.)
618
+
601
619
  **These surfaces verify over HTTP, not from `file://`.** The signal, badge, and
602
620
  table all `fetch()` the chain (and table.json), which the browser blocks on a
603
621
  `file://` page, so opening `index.html` directly leaves them silently
@@ -615,22 +633,30 @@ in the `exports` map), so imports like `tamper-signal/react`, `/table`, and
615
633
  the Node entries (`.`, `/express`) expect `@types/node` as a normal Node
616
634
  project already has.
617
635
 
618
- Any other framework, or plain HTML: the web component. Import
636
+ Any other framework, or plain HTML: the web components. Import
619
637
  `tamper-signal/element` (or vendor `badge/element.js`, which needs light.js
620
- and badge.js beside it) and write one tag:
638
+ and badge.js beside it) for the light, `tamper-signal/room` (or
639
+ `badge/room.js`) for the room, and write one tag each:
621
640
 
622
641
  ```html
623
- <tamper-signal chain="/receipts/chain.json"></tamper-signal>
642
+ <tamper-signal chain="/receipts/chain.json" receipts-href="#tamper-room"></tamper-signal>
643
+ <tamper-signal-room chain="/receipts/chain.json"></tamper-signal-room>
624
644
  ```
625
645
 
626
- Attributes mirror the options: `pub-key`, `watch`, `warn-drift`,
646
+ Light attributes mirror the options: `pub-key`, `watch`, `warn-drift`,
627
647
  `receipts-href`, `surface` (`"light"` default or `"dark"` for a dark host),
628
648
  `invert` (present = shortcut for `surface="dark"`); `theme` is the deprecated
629
- alias of `surface="dark"`.
630
-
631
- Prefer the one-call attach helpers; each serves the receipts directory AND
632
- the bundled browser assets, and returns a `snippet` to render once in the
633
- layout (it mounts the signal into `header`, falling back to `body`):
649
+ alias of `surface="dark"`. Room attributes: `chain` (required), `table`,
650
+ `timeline`, `pub-key`, `watch`, `warn-drift`, `strict`, `max-rows`, `focus`,
651
+ `preset` (`room` | `table` | `console`), `density` (`embedded` | `page`).
652
+ React hosts use `<tamper-signal-room>` straight from JSX (`badge/room.d.ts`
653
+ ships the JSX typing) — import `tamper-signal/room` for the side effect.
654
+
655
+ Prefer the one-call attach helpers; each serves the receipts directory, the
656
+ bundled browser assets, AND the room page at `<assets_prefix>/receipts`, and
657
+ returns a `snippet` to render once in the layout (it mounts the signal into
658
+ `header`, falling back to `body`, with `receiptsHref` pre-wired to the served
659
+ room):
634
660
 
635
661
  ```python
636
662
  # Flask
@@ -659,11 +685,19 @@ These verify SERVER-SIDE with the Python verifier and the pill says so;
659
685
  Streamlit cannot serve the receipts directory for the in-browser walk, and
660
686
  faking the stronger claim would violate rule 1.
661
687
 
662
- Every attach helper also serves the verification console at
663
- `<assets_prefix>/console` (e.g. `/tamper-signal/console`): the chain as an
664
- inspectable pipeline with the break pinned at the severed link, for the
665
- dashboard's builder and for auditors. Mention it to the user when handing
666
- over; it is the page to open when the light is anything but green.
688
+ Every attach helper serves the room at `<assets_prefix>/receipts` (e.g.
689
+ `/tamper-signal/receipts`) and adds `?focus=auto` to the light's link, so a
690
+ red chain opens scrolled to the break exhibit and a yellow one to the first
691
+ caveat. The helper return exposes `roomUrl` and `roomSnippet` (an inline
692
+ embedded room, for a host that renders its own Data tab); `console_url` /
693
+ `consoleUrl` stays reachable, serving the same room with its rail open.
694
+ Opting out (`room=False` / `{ room: false }`) is not recommended; the light
695
+ will link to raw JSON. Mention the room to the user when handing over; it is
696
+ the page to open when the light is anything but green.
697
+
698
+ Hosts that want their own tab chrome: mount `roomSnippet` inside the tab,
699
+ pass `strict`, listen for the bubbling `tamper-signal:state` event, and paint
700
+ your own dot on your own tab — the room never touches host chrome.
667
701
 
668
702
  Manual fallback when no helper fits: serve the directory statically (Flask
669
703
  `static_folder="receipts"`, FastAPI `StaticFiles`, Express
@@ -679,8 +713,8 @@ Options on the fourth argument: `watch` (re-verify every N ms), `warnDrift`,
679
713
  boolean shortcut for `surface: "dark"`); the old `theme: "light"` still works
680
714
  as the `surface: "dark"` alias.
681
715
 
682
- The expandable badge (`renderReceiptBadge(el, "/receipts/chain.json")`) is the
683
- alternative for pages with room for a full-width strip.
716
+ The expandable badge (`renderReceiptBadge`) is deprecated (removed in 3.0):
717
+ mount the light with the room behind it instead.
684
718
 
685
719
  ## 7. Let the signal flag broken metrics
686
720
 
@@ -701,66 +735,60 @@ so their column never reaches `numeric_sums`, and a `data-receipt-column` on it
701
735
  can never flag a change — silently. Grouping isn't auto-stripped on purpose: it
702
736
  would diverge from the Python canonicalization and is locale-ambiguous (`"1,234"`
703
737
  is 1234 or 1.234?). Fix it upstream with a signed normalize stage that strips
704
- the separators before the receipt is written. `tamper-signal ingest` prints a
705
- warning naming any such columns; programmatically, call `groupedNumericColumns(records)`.
706
-
707
- ## 8. The Data tab (when asked for table UI or views)
708
-
709
- The project's stance: a dashboard built on verified data should show the
710
- verified table, not just charts. Two steps:
711
-
712
- 1. After the pipeline runs, export the canonical table document:
713
-
714
- ```bash
715
- # Python
716
- tamper-signal export receipts/chain.json --data path/to/dashboard_data.xlsx
717
- # JavaScript
718
- tamper-signal export receipts/chain.json --data path/to/dashboard_data.csv
719
- ```
720
-
721
- The chain path is positional in both CLIs (the Python CLI also accepts
722
- `--chain receipts/chain.json` for the same value).
723
-
724
- This writes `receipts/table.json` and refuses if the data does not match
725
- the final receipt (the Data tab only ever shows attested data). Re-run it
726
- whenever the pipeline runs, or the tab will honestly report a stale table.
727
- In a JS build you can write it programmatically instead with
728
- `canonicalDocument(finalRecords)` (see step 1b).
729
-
730
- 2. Mount the table (vendor `badge/table.js` beside badge.js with
731
- `tamper-signal assets`, or import `tamper-signal/table`):
732
-
733
- ```html
734
- <script type="module">
735
- import { mountReceiptTable } from "/badge/table.js";
736
- mountReceiptTable(document.querySelector("#data-tab"), "/receipts/chain.json");
737
- </script>
738
- ```
739
-
740
- Or, in plain HTML or any framework, the web component — the parallel of
741
- `<tamper-signal>` for the badge. Importing `tamper-signal/table` (or
742
- `badge/table.js`) registers `<tamper-signal-table>`:
743
-
744
- ```html
745
- <script type="module" src="/badge/table.js"></script>
746
- <tamper-signal-table chain="/receipts/chain.json"></tamper-signal-table>
747
- ```
748
-
749
- Attributes: `chain` (required), `table` (table.json URL; defaults to
750
- table.json beside the chain), `max-rows` (rows before the "show all"
751
- footer, default 500), and `strict` (present = the table emits its verdict
752
- with `strict: true` so the host can gate other views on a broken chain). The
753
- table never blocks UI itself; after each verification it fires a bubbling
754
- `tamper-signal:state` event (and an `onState` callback) carrying
755
- `{ state, attested, strict }`. Default (no `strict`) stays always-on and
756
- always-honest. Recommended host gate: `strict && (state === "red" || !attested)`.
757
-
758
- The component re-hashes the served document in the viewer's browser and
759
- compares it against the final receipt, so VERIFIED means the rows on screen
760
- are byte-for-byte the attested data. It renders its own states: green, yellow
761
- with caveats, chain broken (with the moved columns flagged), and "not the
762
- attested data" when table.json is stale or edited. Design reference:
763
- `designs/03-data-tab.html`.
738
+ the separators before the receipt is written. On Node, `tamper-signal ingest`
739
+ prints a warning naming any such columns, and `groupedNumericColumns(records)`
740
+ finds them in code; the Python CLI also warns on stderr during normal ingest
741
+ (both CLIs keep `--json` stderr silent), and `grouped_numeric_columns(records)`
742
+ is available from `tamper_signal`.
743
+
744
+ ## 8. Publish table.json so the room's landing plane fills
745
+
746
+ The room's primary content is the attested data table. It fills from
747
+ `table.json`, the canonical table document, which the room re-hashes in the
748
+ viewer's browser against the final receipt — so its VERIFIED means the rows
749
+ on screen are byte-for-byte the attested data. Export it as the LAST step of
750
+ every pipeline run:
751
+
752
+ ```bash
753
+ # Python
754
+ tamper-signal export receipts/chain.json --data path/to/dashboard_data.xlsx
755
+ # JavaScript
756
+ tamper-signal export receipts/chain.json --data path/to/dashboard_data.csv
757
+ ```
758
+
759
+ The chain path is positional in both CLIs (the Python CLI also accepts
760
+ `--chain receipts/chain.json` for the same value). The command refuses if the
761
+ data does not match the final receipt — the room only ever shows attested
762
+ data. In a JS build you can write it programmatically instead with
763
+ `canonicalDocument(finalRecords)` (see step 1b).
764
+
765
+ Skipping or forgetting this step degrades honestly, never silently:
766
+
767
+ - **No table.json published**: the room's table plane shows a grey "NO
768
+ ATTESTED TABLE PUBLISHED" slab naming the export command. The chain
769
+ verdict, rail, and drawers still render; grey, because absence is not
770
+ tampering. The emitted state carries `attested: false`.
771
+ - **Stale table.json** (pipeline re-ran, export didn't): the room's verdict
772
+ reads `NOT THE ATTESTED DATA` with the exact re-run command — a
773
+ build-behind state, deliberately distinct from a broken chain.
774
+
775
+ So: wire the export into the pipeline run itself, not a manual step. Both
776
+ wrappers do it for you — pass `exportTable: true` to `rebuildChain(...)`
777
+ (Node), or `write_table=True` on your FINAL `@receipt_step` stage (Python) —
778
+ and `table.json` is written as the last pipeline step, always matching the
779
+ chain tail. If a published table does go stale anyway, `tamper-signal verify`
780
+ prints a one-line stderr reminder naming the re-run command (absence stays
781
+ silent: CLI-only projects never publish a table).
782
+
783
+ Embedding the room inline (instead of the helper-served page): mount
784
+ `mountSignalRoom` / `<tamper-signal-room>` where the old Data tab lived. The
785
+ `table.js` shim (`mountReceiptTable`, `<tamper-signal-table>`) keeps working
786
+ through 2.x and now renders the room's table preset. The state contract is
787
+ unchanged: after each verification the room fires a bubbling
788
+ `tamper-signal:state` event (and an `onState` callback) carrying
789
+ `{ state, attested, strict }`, where `state` is the chain verdict and
790
+ `attested` the byte-identity boolean. The room never blocks UI itself.
791
+ Recommended host gate: `strict && (state === "red" || !attested)`.
764
792
 
765
793
  ## 9. Verify your work before reporting done
766
794
 
@@ -776,14 +804,25 @@ gitignored yourself.) Then confirm the user-visible surfaces:
776
804
  1. `tamper-signal verify receipts/chain.json --pub keys/signing.pub` exits 0.
777
805
  2. Load the host page: the pill reads `VERIFIED · chain intact` (click it for
778
806
  the per-stage popover).
779
- 3. Negative test without touching the user's real chain: this repo commits
780
- known-good fixtures under `examples/chains/` (`intact/` verifies green;
781
- `tampered/` is broken at link 1 -> 2). Point the signal at each to confirm
782
- both states render.
783
- 4. If the pill reads `UNVERIFIED · could not load chain`, the receipts
807
+ 3. Click the pill's `view receipts →` link and confirm it lands in the room
808
+ (the served page or an inline mount), NOT on raw chain.json. If it opens
809
+ unstyled JSON, `receiptsHref` was never wired — fix it (the attach helpers
810
+ do it for you).
811
+ 4. Confirm the room's table plane shows the attested rows with the signed
812
+ control-totals row (not the grey "no attested table published" slab). If
813
+ it shows the slab, the export step (§8) is missing from the pipeline run.
814
+ 5. Negative test without touching the user's real chain: this repo commits
815
+ known-good fixtures under `examples/chains/`. Point the room at
816
+ `tampered/` and confirm the break exhibit leads with business numbers;
817
+ point it at `gap/` and confirm the located caveat card and the ghost node
818
+ in the expanded rail.
819
+ 6. If the pill reads `UNVERIFIED · could not load chain`, the receipts
784
820
  directory is not being served at the URL you passed (or it is blocked by
785
821
  CORS). That state is a capability fallback, not a verdict.
786
822
 
823
+ When you report done, tell the user where the docs and issue tracker live:
824
+ https://github.com/welovejeff/tamper-evident-verification.
825
+
787
826
  ## Troubleshooting
788
827
 
789
828
  - `ChainTailMismatch` when running a wrapped transform: the data fed to the
@@ -802,8 +841,9 @@ gitignored yourself.) Then confirm the user-visible surfaces:
802
841
  |---|---|
803
842
  | `tamper_signal/` | Python package: canonicalization, keys, receipts, verify, `receipt_step` |
804
843
  | `node/` | JavaScript package: same API (`receiptStep`, `verifyChain`), interchangeable chains |
805
- | `badge/badge.js` | Browser verification core + the receipt badge |
844
+ | `badge/badge.js` | Browser verification core (shared by every surface) |
806
845
  | `badge/light.js` | The signal (inline status light), `mountTamperSignal` |
846
+ | `badge/room.js` | The room (the one robust surface), `mountSignalRoom`, `<tamper-signal-room>` |
807
847
  | `badge/light-react.js` | `<TamperSignal />` React wrapper |
808
848
  | `examples/chains/` | Committed known-good and known-broken demo chains |
809
849
  | `designs/` | Working HTML mockups for the signal, console, and Data tab |
package/README.md CHANGED
@@ -2,16 +2,26 @@
2
2
 
3
3
  # The light is green, the data is clean.
4
4
 
5
- [![PyPI](https://img.shields.io/pypi/v/tamper-signal)](https://pypi.org/project/tamper-signal/) [![npm](https://img.shields.io/npm/v/tamper-signal)](https://www.npmjs.com/package/tamper-signal) [![Socket Badge (npm)](https://badge.socket.dev/npm/package/tamper-signal/2.0.0)](https://socket.dev/npm/package/tamper-signal/overview/2.0.0) [![Socket Badge (PyPI)](https://badge.socket.dev/pypi/package/tamper-signal/2.0.0)](https://socket.dev/pypi/package/tamper-signal/overview/2.0.0) [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
5
+ [![PyPI](https://img.shields.io/pypi/v/tamper-signal)](https://pypi.org/project/tamper-signal/) [![npm](https://img.shields.io/npm/v/tamper-signal)](https://www.npmjs.com/package/tamper-signal) [![test](https://github.com/welovejeff/tamper-evident-verification/actions/workflows/test.yml/badge.svg)](https://github.com/welovejeff/tamper-evident-verification/actions/workflows/test.yml) [![Socket Badge (npm)](https://badge.socket.dev/npm/package/tamper-signal/2.1.1)](https://socket.dev/npm/package/tamper-signal/overview/2.1.1) [![Socket Badge (PyPI)](https://badge.socket.dev/pypi/package/tamper-signal/2.1.1)](https://socket.dev/pypi/package/tamper-signal/overview/2.1.1) [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/welovejeff/tamper-evident-verification/blob/main/LICENSE)
6
6
 
7
- 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.
7
+ Tamper Signal is an open-source Python and JavaScript library and CLI that signs a receipt at every stage of a data pipeline and verifies the chain as a green, yellow, or red light: lightweight data provenance for export-to-dashboard pipelines. It can't tell you the data is right, but it can prove nobody changed it.
8
8
 
9
- **Live demo:** [tampersignal.com](https://tampersignal.com/) 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.
9
+ ![The inline status light cycling green, yellow, and red inside a host dashboard, then flagging the unverified metric](https://raw.githubusercontent.com/welovejeff/tamper-evident-verification/main/docs/media/light.gif)
10
10
 
11
- **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.
11
+ *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.*
12
+
13
+ **Try it in your browser, no install:** [tampersignal.com/demo.html](https://tampersignal.com/demo.html) runs every surface on a real receipt chain; flip it to tampered and watch the light catch it. Or `pip install tamper-signal` / `npm install tamper-signal`.
14
+
15
+ **Or run it yourself in a notebook:** [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/welovejeff/tamper-evident-verification/blob/main/examples/quickstart.ipynb) builds a two-stage pandas pipeline with receipts, then shows green, yellow, and red on real output ([`examples/quickstart.ipynb`](examples/quickstart.ipynb)).
16
+
17
+ If this is useful, a star helps other people with AI-built dashboards find it.
18
+
19
+ **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. In Claude Code, `/plugin marketplace add welovejeff/tamper-evident-verification` then `/plugin install tamper-signal@welovejeff` adds an `add-tamper-signal` skill that does the same.
12
20
 
13
21
  ## The problem
14
22
 
23
+ 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 someone hand-edits a file between steps, and the numbers in front of your boss are wrong. Nothing in that workflow catches it. Tamper Signal is the missing verification layer: every stage of the pipeline signs a receipt for what went in and what came out, so a dropped row shows up in that stage's own totals, and one command (or a light on the dashboard itself) tells you whether the chain is intact, or exactly where it broke and by how much.
24
+
15
25
  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.
16
26
 
17
27
  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.
@@ -24,11 +34,11 @@ The badge and the verifier reduce the whole chain to one state:
24
34
  - 🟡 **Yellow.** Verifiable, but with caveats: gaps in receipt coverage, an unrecognized signing key, or control-total drift that needs a human look.
25
35
  - 🔴 **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)`).
26
36
 
27
- ![The inline status light cycling green, yellow, and red inside a host dashboard, then flagging the unverified metric](docs/media/light.gif)
37
+ Honest status: all three verdicts are implemented in `tamper-signal verify` and the browser surfaces. 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 animations in this README are renders of the design mockups in `designs/`; the interfaces they depict have since shipped and, as of 2.1, unified. The surfaces also render a separate grey 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.
28
38
 
29
- *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.*
39
+ ## One light, one room
30
40
 
31
- Honest status: all three verdicts are implemented in `tamper-signal 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 animations in this README are renders of the design mockups in `designs/`; the interfaces they depict have since shipped (`badge/light.js`, `badge/table.js`, `badge/console.js`). 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.
41
+ The browser UI is two things, always shipped together. **The light** (`badge/light.js`) is the whole footprint on your dashboard: a small dark pill in the header. **The room** (`badge/room.js`, since 2.1) is the one surface behind it, where the pill's "view receipts →" lands: the attested data table as the landing plane (re-hashed in the viewer's browser against the final receipt), with the chain as a provenance rail, the break exhibit in business numbers when something is wrong, and the receipt inspector, CLI-mirror event log, chain-of-custody timeline, and "Take your data" evidence export one drawer away. Green earns silence; the layout leads with whatever the verdict demands. The attach helpers serve the room automatically and wire the light to it, so the default integration is both halves in one call. (`tamper-signal/table` and `tamper-signal/console` from 2.0 keep working as room presets.)
32
42
 
33
43
  ## 60-second quickstart
34
44
 
@@ -36,11 +46,10 @@ Python 3.11+. Open source (MIT).
36
46
 
37
47
  ```bash
38
48
  pip install tamper-signal
39
- git clone https://github.com/welovejeff/tamper-evident-verification && cd tamper-evident-verification
40
49
  tamper-signal demo
41
50
  ```
42
51
 
43
- `tamper-signal 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.
52
+ `tamper-signal 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 totals delta). It works in its own `tamper-signal-demo/` folder, so your own keys and receipts are never touched. Run it from a clone of the repo (`git clone https://github.com/welovejeff/tamper-evident-verification && cd tamper-evident-verification`) and it finishes by serving the badge at `http://localhost:8000/badge/badge.html` so you can see green, yellow, and red side by side.
44
53
 
45
54
  > **`tamper-signal: command not found`?** pip installed the script into a bin directory that is not on PATH (common on the python.org framework Python, the default macOS download). Either run it through the same interpreter, `python3 -m tamper_signal verify ...` (works as a drop-in for every `tamper-signal ...` command), or link it onto PATH once: `sudo ln -sf "$(python3 -c 'import sysconfig;print(sysconfig.get_path("scripts"))')/tamper-signal" /usr/local/bin/tamper-signal`.
46
55
 
@@ -54,7 +63,7 @@ tamper-signal diff # compare two runs: code-hash changes and tot
54
63
  tamper-signal log # archived run history as a per-metric trend across runs (read-only)
55
64
  tamper-signal doctor # integration self-check with actionable fixes
56
65
  tamper-signal serve # serve receipts/ on localhost with CORS (dev only)
57
- tamper-signal assets --out badge/ # vendor the browser surfaces (light/badge/element/table/console.js) into a project
66
+ tamper-signal assets --out badge/ # vendor the browser surfaces (light/badge/element/table/console/room.js) into a project
58
67
  tamper-signal annotate --reason "backfill approved" --author dana # sign a reason onto a receipt (chain of custody)
59
68
  tamper-signal watch --config feed.json --out receipts/ # poll a live feed onto the chain (needs [watch]; see below)
60
69
  ```
@@ -89,7 +98,7 @@ const clean = receiptStep(
89
98
  const output = await clean(loadCsv("export.csv"));
90
99
  ```
91
100
 
92
- 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`.
101
+ 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/room`, `tamper-signal/element`, `tamper-signal/react` (plus the 2.0 `tamper-signal/table` and `tamper-signal/console`, now room presets).
93
102
 
94
103
  ## How the chain works
95
104
 
@@ -128,7 +137,7 @@ Hashes say "broken." Totals say "how broken."
128
137
 
129
138
  `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.
130
139
 
131
- ![Receipt badge: green intact chain and red broken chain](badge/badge-demo.png)
140
+ ![Receipt badge: green intact chain and red broken chain](https://raw.githubusercontent.com/welovejeff/tamper-evident-verification/main/badge/badge-demo.png)
132
141
 
133
142
  Green collapsed state reads like: `✓ Verified · TikTok export, May 2026 · 48,212 rows · 2 transforms · chain intact`. Expanding shows one row per receipt.
134
143
 
@@ -157,7 +166,7 @@ We think any dashboard built on verified data should let you see the data. Not a
157
166
 
158
167
  It ships: `tamper-signal export` writes the canonical table document next to the chain (refusing data that does not match the final receipt), and `mountReceiptTable(el, "/receipts/chain.json")` from `badge/table.js` (npm: `tamper-signal/table`) renders it after re-hashing it in the viewer's browser against the final receipt. VERIFIED means the rows on screen are byte-for-byte the attested data; a stale or edited table.json renders dimmed under a "not the attested data" strip, and a broken chain flags the columns that moved at the break. Live demo: `badge/table.html`.
159
168
 
160
- ![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)
169
+ ![The Data tab: the dashboard flips to a dark raw-table view where a broken chain is localized to the views column](https://raw.githubusercontent.com/welovejeff/tamper-evident-verification/main/docs/media/data-tab.gif)
161
170
 
162
171
  *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.*
163
172
 
@@ -171,15 +180,15 @@ To bring an updated file back, `tamper-signal ingest --as replace|period`. `repl
171
180
 
172
181
  The light answers "is it fine?"; the console answers "where, exactly, and by how much?" `mountReceiptConsole(el, "/receipts/chain.json")` from `badge/console.js` (npm: `tamper-signal/console`) renders the chain as an inspectable pipeline: links carry the hash they proved, a break severs the link with the break card pinned at it, coverage gaps appear as ghost nodes at their position, and the event log mirrors `tamper-signal verify` line for line. Every attach helper also serves it ready-made at `/tamper-signal/console`. Live demo: `badge/console.html`.
173
182
 
174
- ![The verification console: a pipeline of signed receipts where a tampered stage severs the chain at the exact link](docs/media/console.gif)
183
+ ![The verification console: a pipeline of signed receipts where a tampered stage severs the chain at the exact link](https://raw.githubusercontent.com/welovejeff/tamper-evident-verification/main/docs/media/console.gif)
175
184
 
176
185
  *The verification console: calm when green, surgical when red.*
177
186
 
178
- Below the pipeline the console renders the **chain of custody**: the imports and changes, each signed reason attached to the receipt it explains (`tamper-signal annotate`), and any changes **awaiting human review**. It is an additive layer over the published `timeline.json` — it never feeds the verdict above.
187
+ Below the pipeline the console renders the **chain of custody**: the imports and changes, each signed reason attached to the receipt it explains (`tamper-signal annotate`), and any changes **awaiting human review**. It is an additive layer over the published `timeline.json`; it never feeds the verdict above.
179
188
 
180
189
  ## Live-source watcher (optional)
181
190
 
182
- When the source is a live feed rather than a file you re-export by hand, the watcher keeps it on the same signed chain. `tamper-signal watch` (behind `pip install "tamper-signal[watch]"`) polls an HTTP/JSON-API or RSS/Atom endpoint, judges the new data against the declared band/settle, and **auto-appends only a clean change**. A retroactive edit to an already-settled period — or a slow drift that cumulatively breaches the band — is never signed unattended: it is withheld as a signed *pending event* and paused for a human.
191
+ When the source is a live feed rather than a file you re-export by hand, the watcher keeps it on the same signed chain. `tamper-signal watch` (behind `pip install "tamper-signal[watch]"`) polls an HTTP/JSON-API or RSS/Atom endpoint, judges the new data against the declared band/settle, and **auto-appends only a clean change**. A retroactive edit to an already-settled period, or a slow drift that cumulatively breaches the band, is never signed unattended: it is withheld as a signed *pending event* and paused for a human.
183
192
 
184
193
  ```bash
185
194
  pip install "tamper-signal[watch]"
@@ -188,7 +197,7 @@ tamper-signal review #
188
197
  tamper-signal review accept <hash> --reason "confirmed by finance" # sign off + commit
189
198
  ```
190
199
 
191
- The fetch is SSRF-hardened (public hosts only, redirects off, TLS verified, byte + wall-clock caps; RSS parsed through `defusedxml`), change detection uses a full-content fingerprint rather than a trust-me `ETag`, and the unattended commit is crash-safe. The recommended deployment is the stateless tick under a systemd timer or cron, so the signing key is not resident between runs — see `AGENTS.md` §5c for a hardened unit. `watch`/`review` are Python-only today; the chains they write are read and verified by the JavaScript stack unchanged.
200
+ The fetch is SSRF-hardened (public hosts only, redirects off, TLS verified, byte + wall-clock caps; RSS parsed through `defusedxml`), change detection uses a full-content fingerprint rather than a trust-me `ETag`, and the unattended commit is crash-safe. The recommended deployment is the stateless tick under a systemd timer or cron, so the signing key is not resident between runs (see `AGENTS.md` §5c for a hardened unit). `watch`/`review` are Python-only today; the chains they write are read and verified by the JavaScript stack unchanged.
192
201
 
193
202
  ## Anchoring (optional)
194
203
 
@@ -211,4 +220,4 @@ Those tools model lineage and quality at the warehouse and orchestration layer.
211
220
 
212
221
  ## Contributing
213
222
 
214
- 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.
223
+ 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, PRs, and stars all help; [CONTRIBUTING.md](https://github.com/welovejeff/tamper-evident-verification/blob/main/CONTRIBUTING.md) covers setup and the few rules that keep both stacks in step. The original Luhn hash demo lives unchanged in `legacy/` and is off the main path.