@taifoon/jev 0.2.18 → 0.4.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/CHANGELOG.md ADDED
@@ -0,0 +1,32 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@taifoon/jev`. Versions follow the GitHub releases of this repository; each is published to npm
4
+ with provenance by `.github/workflows/publish.yml`.
5
+
6
+ ## 0.4.0
7
+
8
+ - `verify` reads a trusted record past an endpoint that serves only recent logs. `JevAnswerLog.recordedAt` names the block
9
+ of a trusted record; when an endpoint answers `[]` for that block, the next endpoint of the rotation is asked. A record
10
+ reads `not_recorded` only when every endpoint that answered held no row; no answer at all is still `rpc_unavailable`.
11
+ (Seen on Base for a record 1.35M blocks old.) Two tests cover both outcomes.
12
+ - `src/attest.ts`: the defaults of an unknown chain are their own typed value, so the copy compiles under stricter
13
+ TypeScript settings (TS2783). Types only; the calls it returns are unchanged.
14
+ - Taifoon mainnet (3692781) is Jev's record home: `record()` builds the `JevAnswerLog.record` and `JevDecisionLog.record`
15
+ calls for it by default (JevDecisionLog `0xaEbF9084Bb32eB2E0F58537526A8877dd1436b23`, JevAnswerLogV2
16
+ `0xC08dfc61d702AEA45d1EF2515d8BDA36d548C75c`), and `verify --chain taifoon` reads them. It has no GradeStampRegistry
17
+ yet; the stamp is listed under `missing`.
18
+ - Base: answers are written to JevAnswerLogV2 (`0xC08dfc61d702AEA45d1EF2515d8BDA36d548C75c`, from block 52639649).
19
+ `verify` reads V2 first, then the earlier JevAnswerLog (`0x8e9B9cE86a2d55c10318607b0c815B7B8C66254d`), so every
20
+ earlier record still verifies. Two tests cover both logs.
21
+ - README: what you get, how it works, and where Jev's records live.
22
+ - package: CHANGELOG.md ships in the tarball; search keywords.
23
+
24
+ ## 0.3.0 (2026-10-14)
25
+
26
+ - Recording is on by default: every grade returns the unsigned `JevAnswerLog.record`, `JevDecisionLog.record` and
27
+ `GradeStampRegistry.stamp` calls for the Taifoon devnet 36927 and the Taifoon mainnet 3692781 (`--record none` turns
28
+ them off; a contract a network does not have is listed in `missing`). One shared attestation helper for the mark, the
29
+ attestation and the stamp.
30
+ - Optional fields of the attestation helper accept `undefined` (`exactOptionalPropertyTypes`).
31
+
32
+ Earlier versions: see the [releases](https://github.com/taifoon-io/jev/releases).
package/README.md CHANGED
@@ -1,24 +1,83 @@
1
- # @taifoon/jev
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/taifoon-io/jev/main/assets/banner.svg" alt="Jev for agent jobs" width="100%">
3
+ </p>
4
+
5
+ <p align="center">
6
+ <a href="https://www.npmjs.com/package/@taifoon/jev"><img src="https://img.shields.io/npm/v/@taifoon/jev?style=flat-square&labelColor=000000&color=3DA5FF&label=npm" alt="npm"></a>
7
+ <a href="https://www.npmjs.com/package/@taifoon/jev#provenance"><img src="https://img.shields.io/badge/provenance-signed-00B5B8?style=flat-square&labelColor=000000" alt="provenance"></a>
8
+ <a href="LICENSE"><img src="https://img.shields.io/npm/l/@taifoon/jev?style=flat-square&labelColor=000000&color=E6F0F7" alt="licence"></a>
9
+ <a href="https://www.npmjs.com/package/@taifoon/jev"><img src="https://img.shields.io/npm/dm/@taifoon/jev?style=flat-square&labelColor=000000&color=3DA5FF" alt="downloads"></a>
10
+ </p>
2
11
 
3
12
  **Did the AI agent do the work it was paid for? Ask Jev, and get an answer you can check.**
4
13
 
5
- - **What:** a grader for agent jobs. Code checks the facts, TypeSafe's Jev answers four closed questions about the
6
- delivery, and code turns the answers into complete, reject or needs review, with a receipt anyone can recompute.
7
- - **Why:** agents are paid on chain for work nobody reads. A grade with its full probabilities, a published rubric and
8
- an appeal path is something a buyer, a seller and a contract can all rely on.
9
- - **How:** replay a Base job, graded by Jev, offline and with no key:
14
+ Code checks the facts of the delivery, TypeSafe's Jev answers four closed questions about it, and code turns the answers
15
+ into **complete**, **reject** or **needs review**, with a receipt anyone can recompute, the unsigned calls that record it on
16
+ chain and the one call that ends the job on its protocol (ERC-8183, Virtuals ACP, BitAgent). No key inside, and it signs
17
+ nothing.
10
18
 
11
- ```
19
+ ## 30 seconds
20
+
21
+ ```sh
12
22
  npx @taifoon/jev run --demo
13
23
  ```
14
24
 
25
+ Offline, no key: replays BitAgent job 7287 on Base as Jev graded it, composes **reject**, and arrives at the decision
26
+ digest that is anchored on chain. Then read a recorded grade back from Base:
27
+
28
+ ```sh
29
+ npx @taifoon/jev verify decision-1790522234088-10691d5017 --network base
30
+ ```
31
+
32
+ It recomputes the record's digests offline and finds both rows on Base (`"status": "recorded"`).
33
+
15
34
  ![npx @taifoon/jev run --demo: a BitAgent job on Base, graded by Jev, checked against the chain](https://raw.githubusercontent.com/taifoon-io/jev/main/docs/demo.gif)
16
35
 
17
- Then grade your own: with no key, on your free grades on the Taifoon coordination layer (3 a day per caller), or with
18
- your TypeSafe key from [console.typesafe.ai](https://console.typesafe.ai). Installing it also
19
- installs Taifoon's other packages (@taifoon/jev-wilson, @taifoon/n8n-nodes-typesafe). No key inside, and it signs nothing. Recording a grade on chain is optional.
36
+ ## What you get
37
+
38
+ | | |
39
+ |---|---|
40
+ | **A verdict with its reasons** | complete, reject or needs review, with every answer's full probability distribution |
41
+ | **A receipt anyone can recompute** | digests of the evidence, the answers and the decision; `verify()` recomputes them offline |
42
+ | **The on-chain record, unsigned** | `JevAnswerLog.record`, `JevDecisionLog.record` and `GradeStampRegistry.stamp` calls for your wallet |
43
+ | **The call that ends the job** | the evaluator call for `assurance-hook`, `judge-adapter`, `virtuals-erc8183`, `virtuals-memo-acp`, `bitagent-erc8183` |
44
+ | **A cover price for the next job** | the seller's premium from its record, through [`@taifoon/jev-wilson`](https://www.npmjs.com/package/@taifoon/jev-wilson) |
45
+ | **No key needed to start** | 3 free grades a day per caller on the Taifoon coordination layer, or your own TypeSafe key |
46
+
47
+ ## How it works
48
+
49
+ ```
50
+ the job as the buyer wrote it (task, criteria, source, delivered)
51
+ │ prepareJob()
52
+ ▼
53
+ code checks the facts ─── a check fails ──▶ reject (Jev is not asked)
54
+ │ all pass
55
+ ▼
56
+ Jev answers RUBRIC_v1: spec_met · unsupported_claim · ending · cheat_shaped
57
+ │ grade()
58
+ ▼
59
+ code composes THRESHOLDS_v1 ──▶ complete | reject | needs_review + receipt
60
+ │ record() · evaluatorCall()
61
+ ▼
62
+ unsigned calls: the answer log, the decision log, the evaluator seat (you sign)
63
+ │ verify()
64
+ ▼
65
+ digests recomputed, rows found on chain
66
+ ```
67
+
68
+ ## Install
69
+
70
+ ```sh
71
+ npm i @taifoon/jev
72
+ ```
73
+
74
+ Node.js 20 or later. Installing it also installs Taifoon's other packages (@taifoon/jev-wilson,
75
+ @taifoon/n8n-nodes-typesafe); nothing third-party. Grade with no key on your free grades on the Taifoon coordination layer
76
+ (3 a day per caller), or with your TypeSafe key from [console.typesafe.ai](https://console.typesafe.ai). Every grade comes
77
+ with the unsigned calls that record it on chain (the Taifoon devnet and mainnet by default, any network you name;
78
+ `--record none` turns them off).
20
79
 
21
- ## Quick start
80
+ ## Grade your own job
22
81
 
23
82
  An agent was hired for a job. The grader answers one question: did it do the work as the task laid it out? You give
24
83
  it the job the way the buyer wrote it, and it does the rest:
@@ -106,7 +165,7 @@ the output says how to retry. Set `record` to `none` to grade without writing on
106
165
 
107
166
  ## `jev run` flags
108
167
 
109
- Safe by default: your own TypeSafe key only, nothing recorded, nothing signed or sent. A typo is refused, never guessed.
168
+ Safe by default: nothing is signed or sent. The calls that record and stamp the grade come back unsigned (`--record none` turns them off). A typo is refused, never guessed.
110
169
 
111
170
  | Flag | Default | What it does |
112
171
  |---|---|---|
@@ -115,7 +174,7 @@ Safe by default: your own TypeSafe key only, nothing recorded, nothing signed or
115
174
  | `--job-file job.json` | | Grade a job you describe (see `examples/jobs/`) |
116
175
  | `--evidence pack.json` | | Grade your own evidence pack, no layer |
117
176
  | `--answers answers.json` | | Answers you already have (e.g. from the n8n node); Jev is not asked |
118
- | `--record none\|devnet\|base\|both` | `none` | Opt in to the unsigned calls that record the grade on chain |
177
+ | `--record none\|devnet\|base\|both` | Taifoon devnet + mainnet | Where the unsigned calls that record and stamp the grade point; `none` turns them off |
119
178
  | `--layer <https url>` / `--no-layer` | `https://coord.taifoon.dev` | Where jobs and quotes are read |
120
179
  | `--protocol <name>` | from the job id | Evaluator seat: `virtuals-erc8183`, `virtuals-memo-acp`, `bitagent-erc8183`, `assurance-hook`, `judge-adapter` |
121
180
  | `--price-usdc <n>` | the job's budget | Price for the premium quote |
@@ -182,8 +241,30 @@ npx @taifoon/jev workflows export ./jev-workflows
182
241
  ## Chains
183
242
 
184
243
  `verify` reads the Jev logs on every chain they are deployed to: the Taifoon devnet (36927), Base (8453), Arbitrum One
185
- (42161), Arc (5042) and Monad (143). Robinhood Chain (4663), Taifoon mainnet (3692781) and Ethereum (1) are known
186
- chains with no Jev log yet; `verify` says so instead of reading them.
244
+ (42161), Arc (5042) and Monad (143). Robinhood Chain (4663) and Ethereum (1) are known chains with no Jev log; `verify`
245
+ says so instead of reading them.
246
+
247
+ ### Where Jev's records live
248
+
249
+ | Chain | JevDecisionLog | JevAnswerLog |
250
+ |---|---|---|
251
+ | Taifoon mainnet (3692781), the record home | `0xaEbF9084Bb32eB2E0F58537526A8877dd1436b23` | `0xC08dfc61d702AEA45d1EF2515d8BDA36d548C75c` (JevAnswerLogV2) |
252
+ | Base (8453) | [`0x209490d6A0FFC5368A42b0c2208BDCda853f6a92`](https://basescan.org/address/0x209490d6A0FFC5368A42b0c2208BDCda853f6a92) | [`0xC08dfc61d702AEA45d1EF2515d8BDA36d548C75c`](https://basescan.org/address/0xC08dfc61d702AEA45d1EF2515d8BDA36d548C75c) (JevAnswerLogV2); earlier records in [`0x8e9B9cE86a2d55c10318607b0c815B7B8C66254d`](https://basescan.org/address/0x8e9B9cE86a2d55c10318607b0c815B7B8C66254d) |
253
+ | Taifoon devnet (36927) | `0x1D622511862DD7DEffB8fEeBc225E0FAdA1e9A05` | `0xD8c1d8188Fc8EE388792f6EB3B3dbd4f341Ba8e3` |
254
+
255
+ The two Taifoon mainnet logs were deployed on 2026-11-08 at their CREATE2 addresses (JevAnswerLogV2 has the same
256
+ address on Base). Taifoon mainnet has no public explorer yet; read them from its public RPC:
257
+
258
+ ```sh
259
+ curl -s https://mainnet.taifoon.dev -H 'content-type: application/json' \
260
+ -d '{"jsonrpc":"2.0","id":1,"method":"eth_getCode","params":["0xaEbF9084Bb32eB2E0F58537526A8877dd1436b23","latest"]}'
261
+ ```
262
+
263
+ `record()` builds the calls for both logs on Taifoon mainnet by default (next to the devnet), and `verify --chain taifoon`
264
+ reads them. Taifoon mainnet has no GradeStampRegistry yet, so the stamp is listed under `missing` there, said rather
265
+ than faked. On Base, answers are written to JevAnswerLogV2 and `verify` reads it first, then the earlier JevAnswerLog,
266
+ so a record from before V2 still verifies. Base keeps its published JevDecisionLog. Every address above is in
267
+ `src/addresses.ts` and `src/attest.ts`.
187
268
 
188
269
  | Flag | What it does |
189
270
  |---|---|
@@ -213,6 +294,12 @@ it ships (`src/rpc-fallback.ts`, generated from the RPC registry).
213
294
  In code: `verify(digest, { network: 'arbitrum' })` (a name or a chain id), `{ rpc: 'https://…' }`, and `JevChainError`
214
295
  (`code`: `rpc_unavailable`, `not_deployed`, `reverted`, `bad_input`) from the lower-level calls.
215
296
 
297
+ ## Links
298
+
299
+ - npm: [@taifoon/jev](https://www.npmjs.com/package/@taifoon/jev) · pricing: [@taifoon/jev-wilson](https://www.npmjs.com/package/@taifoon/jev-wilson) · n8n: [@taifoon/n8n-nodes-typesafe](https://www.npmjs.com/package/@taifoon/n8n-nodes-typesafe)
300
+ - The coordination layer: [coord.taifoon.dev/v1](https://coord.taifoon.dev/v1) ([OpenAPI](https://coord.taifoon.dev/v1/openapi.json)) · [taifoon.io](https://www.taifoon.io)
301
+ - Changes: [CHANGELOG.md](CHANGELOG.md) · issues: [github.com/taifoon-io/jev/issues](https://github.com/taifoon-io/jev/issues) · contact: info@taifoon.io
302
+
216
303
  ## Licence
217
304
 
218
305
  Independent project. Jev and TypeSafe are products of TypeSafe AI, Inc., which does not endorse this package.
package/bin/run.mjs CHANGED
@@ -6,7 +6,8 @@
6
6
  // jev run --demo offline, no key: replays a REAL graded job (BitAgent 7287 on Base) with Jev's
7
7
  // recorded answers, and checks the result against the decision on chain
8
8
  // jev run --answers answers.json answers you already have (the n8n TypeSafe node's output): Jev is not asked
9
- // --record none|devnet|base|both opt-in: where the grade is recorded (default none: the receipt and digests only);
9
+ // --record none|devnet|base|both where the grade is recorded (default: unsigned calls for the Taifoon devnet and
10
+ // mainnet; none = the receipt and digests only);
10
11
  // --network is the same flag
11
12
  // --layer <url> | --no-layer the coordination layer (default https://coord.taifoon.dev)
12
13
  // --yes run every step without asking; --json print the whole trace as JSON at the end
@@ -27,7 +28,7 @@ function factOf(r) {
27
28
  case 'evidence': return `${o.facts} labelled facts · ${o.gaps?.length ?? 0} gaps · ${o.state_chars} characters for Jev to read`;
28
29
  case 'facts': return `delivered ${o.delivered ? 'yes' : 'no'} · checks ${o.checksOk === false ? 'failed' : o.checksOk ? 'ok' : 'none'}${o.priceUsdc != null ? ` · ${o.priceUsdc} USDC` : ''}`;
29
30
  case 'grade': return `${o.verdict} · ${o.reasons?.[0] ?? ''}${o.model ? ` · ${o.model}` : ''}`;
30
- case 'record': return `${o.status === 'none' ? 'not recorded (opt in with --record)' : `${o.status} · ${(o.calls ?? []).join(', ')}`}${o.recording?.held ? ` · layer held it: ${o.recording.held}` : ''}${typeof o.layer === 'object' && o.layer ? ` · layer ${short(o.layer.digest)}` : ''}`;
31
+ case 'record': return `${o.status === 'none' ? 'not recorded (--record none)' : `${o.status} · ${(o.calls ?? []).join(', ')}`}${o.recording?.held ? ` · layer held it: ${o.recording.held}` : ''}${typeof o.layer === 'object' && o.layer ? ` · layer ${short(o.layer.digest)}` : ''}`;
31
32
  case 'evaluator': return `${o.fn} on ${o.protocol} · signed by ${o.signer}`;
32
33
  case 'premium': return `${o.guaranteed ? 'guaranteed' : 'not guaranteed'} · premium ${o.premium_label ?? o.premium_ratio}`;
33
34
  case 'verify': return o.ok ? `the receipt re-derives · ${Object.keys(o.checks ?? {}).length} checks` : `problems: ${(o.problems ?? []).join('; ')}`;
@@ -55,10 +56,10 @@ export async function run(argv) {
55
56
  const jobFile = file('--job-file');
56
57
  const ansFile = file('--answers');
57
58
  const rec = demo ? JSON.parse(readFileSync(new URL('../examples/jobs/base-bitagent-7287.recorded.json', import.meta.url), 'utf8')) : null;
58
- const network = val('--record') ?? val('--network') ?? 'none'; const layer = noLayer ? false : val('--layer');
59
+ const network = val('--record') ?? val('--network') ?? undefined; const layer = noLayer ? false : val('--layer');
59
60
  const protocol = val('--protocol'); const price = val('--price-usdc'); const sellerRec = val('--seller-record');
60
61
  if (a.length) refuse(`unknown flag ${a[0]}`);
61
- if (!['none', 'devnet', 'base', 'both'].includes(network)) refuse(`--record is none, devnet, base or both (got ${network})`);
62
+ if (network !== undefined && !['none', 'devnet', 'base', 'both'].includes(network)) refuse(`--record is none, devnet, base or both (got ${network})`);
62
63
  if (demo && (job || evFile || jobFile || ansFile)) refuse('--demo replays one recorded job: drop --job, --job-file, --evidence and --answers');
63
64
  if ([job, evFile, jobFile].filter(Boolean).length > 1) refuse('pick one source: --job, --job-file or --evidence');
64
65
  if (job && !/^\d+:.+$/.test(job) && !/^\S+$/.test(job)) refuse(`--job is <chain>:<id>, e.g. 8453:bitagent:8453:7287 (got ${job})`);
@@ -71,7 +72,7 @@ export async function run(argv) {
71
72
 
72
73
  const t = createTerm({ quiet: json });
73
74
  const rl = yes ? null : createInterface({ input: process.stdin, output: process.stdout });
74
- t.info(`jev run · ${noLayer ? 'offline' : `layer ${layer ?? 'https://coord.taifoon.dev'}`} · grade on ${demo ? `Jev's recorded answers for ${rec.job.ref} (${rec.job.task})` : ansFile ? 'supplied answers' : process.env.TYPESAFE_KEY ? 'your TypeSafe key' : noLayer ? 'no key: set TYPESAFE_KEY, or try --demo' : 'your free grades on the layer (no TypeSafe key)'} · record → ${network === 'none' ? 'none (opt in with --record devnet|base|both)' : network}${process.env.TAIFOON_RELAYER_KEY ? ' + the layer' : ''}`);
75
+ t.info(`jev run · ${noLayer ? 'offline' : `layer ${layer ?? 'https://coord.taifoon.dev'}`} · grade on ${demo ? `Jev's recorded answers for ${rec.job.ref} (${rec.job.task})` : ansFile ? 'supplied answers' : process.env.TYPESAFE_KEY ? 'your TypeSafe key' : noLayer ? 'no key: set TYPESAFE_KEY, or try --demo' : 'your free grades on the layer (no TypeSafe key)'} · record → ${network === undefined ? 'Taifoon devnet + mainnet (unsigned calls; --record none turns it off)' : network}${process.env.TAIFOON_RELAYER_KEY ? ' + the layer' : ''}`);
75
76
 
76
77
  const trace = await pipeline({
77
78
  layer, job, network, protocol, priceUsdc: price ? Number(price) : undefined, sellerRecord: recM ? { k: Number(recM[1]), n: Number(recM[2]) } : undefined,
@@ -60,7 +60,8 @@ export declare const REGISTRY_ADDRESSES: {
60
60
  readonly block: 48395723;
61
61
  };
62
62
  };
63
- /** The Jev logs on every chain the registry deploys them to (JevAnswerLog + JevDecisionLog, from their deploy block). */
63
+ /** The Jev logs on every chain the registry deploys them to (from their deploy block): the answer log records go to (JevAnswerLogV2
64
+ * where it is live, else JevAnswerLog), the decision log, and the earlier answer logs still read (Base: the first JevAnswerLog). */
64
65
  export declare const JEV_LOGS: Readonly<Record<number, {
65
66
  answerLog: {
66
67
  address: string;
@@ -70,4 +71,8 @@ export declare const JEV_LOGS: Readonly<Record<number, {
70
71
  address: string;
71
72
  fromBlock: number;
72
73
  };
74
+ earlierAnswerLogs?: readonly {
75
+ address: string;
76
+ fromBlock: number;
77
+ }[];
73
78
  }>>;
package/dist/addresses.js CHANGED
@@ -22,11 +22,13 @@ export const REGISTRY_ADDRESSES = {
22
22
  /** standards-erc8183 · BitAgent ERC-8183 escrow (live) */
23
23
  bitagentErc8183: { chainId: 8453, address: '0x5009ABB3A309115a4a682C66BAf3BC9E0329BaB7', block: 48395723 },
24
24
  };
25
- /** The Jev logs on every chain the registry deploys them to (JevAnswerLog + JevDecisionLog, from their deploy block). */
25
+ /** The Jev logs on every chain the registry deploys them to (from their deploy block): the answer log records go to (JevAnswerLogV2
26
+ * where it is live, else JevAnswerLog), the decision log, and the earlier answer logs still read (Base: the first JevAnswerLog). */
26
27
  export const JEV_LOGS = {
27
28
  143: { answerLog: { address: '0x29fEEc116e0CAbbaf38e9EBF2BBf65d82D672104', fromBlock: 110216088 }, decisionLog: { address: '0x1aC6266b9926cB3E206D5dc0c9d1B7c873aa150a', fromBlock: 110216093 } },
28
29
  5042: { answerLog: { address: '0x45214361A8cADb8aEe6302cd5653E119E5a89839', fromBlock: 24071071 }, decisionLog: { address: '0x863db7dAc69CC614a98822082cbe884F1f3b55cA', fromBlock: 24071074 } },
29
- 8453: { answerLog: { address: '0x8e9B9cE86a2d55c10318607b0c815B7B8C66254d', fromBlock: 51863008 }, decisionLog: { address: '0x209490d6A0FFC5368A42b0c2208BDCda853f6a92', fromBlock: 51856200 } },
30
+ 8453: { answerLog: { address: '0xC08dfc61d702AEA45d1EF2515d8BDA36d548C75c', fromBlock: 52639649 }, decisionLog: { address: '0x209490d6A0FFC5368A42b0c2208BDCda853f6a92', fromBlock: 51856200 }, earlierAnswerLogs: [{ address: '0x8e9B9cE86a2d55c10318607b0c815B7B8C66254d', fromBlock: 51863008 }] },
30
31
  36927: { answerLog: { address: '0xD8c1d8188Fc8EE388792f6EB3B3dbd4f341Ba8e3', fromBlock: 1106754 }, decisionLog: { address: '0x1D622511862DD7DEffB8fEeBc225E0FAdA1e9A05', fromBlock: 981099 } },
31
32
  42161: { answerLog: { address: '0x29fEEc116e0CAbbaf38e9EBF2BBf65d82D672104', fromBlock: 511340178 }, decisionLog: { address: '0x1aC6266b9926cB3E206D5dc0c9d1B7c873aa150a', fromBlock: 511340188 } },
33
+ 3692781: { answerLog: { address: '0xC08dfc61d702AEA45d1EF2515d8BDA36d548C75c', fromBlock: 2598386 }, decisionLog: { address: '0xaEbF9084Bb32eB2E0F58537526A8877dd1436b23', fromBlock: 2598383 } },
32
34
  };
@@ -0,0 +1,129 @@
1
+ /**
2
+ * attest: the attested mark, the on-chain attestation and the stamp of one step (a grade, an assurance settle, a resale
3
+ * step), as UNSIGNED calls aimed at a default network chosen by profile. _ATTEST_BY_DEFAULT_v1_
4
+ *
5
+ * ONE file with NO imports, pure and browser-safe, like assurance.ts: @taifoon/jev, @taifoon/assurance (and through its
6
+ * sync, taifoon-sdk and moonbeam-sdk) carry a byte-identical copy and pin its hash. The addresses come from the address
7
+ * registry (packages/addresses) through scripts/gen-attest.mjs; nothing below the generated block names an address.
8
+ *
9
+ * mark what is attested, offline: the step's kind, the subject ids (decision log + stamp registry) and the
10
+ * digest = keccak256 of the canonical JSON body (or the digest the caller already has, e.g. Jev's).
11
+ * attestation JevDecisionLog.record(subject, kind, digest, confidenceBps, model, uri) on every target network, after
12
+ * JevAnswerLog.record(...) when the step carries Jev's answers.
13
+ * stamp GradeStampRegistry.stamp(subject, digest, verdict, mode, model, uri) when the step has a verdict.
14
+ *
15
+ * Recording is ON by default (the owner's rule of 2026-10-14): profile taifoon → the Taifoon devnet 36927 and the Taifoon
16
+ * mainnet 3692781 (GRID); profile moonbeam → Base 8453. Any network can be named instead; `networks: 'none'` opts out
17
+ * (the mark is still returned). A contract the registry does not have on a target network is listed in `missing`, with
18
+ * no call: nothing is faked. Nothing here signs or sends; the caller signs the calls.
19
+ */
20
+ /** The attestation contracts on every chain the address registry has one on (superseded deploys left out). */
21
+ export declare const ATTEST_TARGETS: Readonly<Record<number, {
22
+ name: string;
23
+ decisionLog: string | null;
24
+ answerLog: string | null;
25
+ stampRegistry: string | null;
26
+ }>>;
27
+ /** The default networks per profile. Taifoon: its devnet and its mainnet (GRID). Moonbeam: Base only. */
28
+ export declare const ATTEST_PROFILES: {
29
+ readonly taifoon: readonly [36927, 3692781];
30
+ readonly moonbeam: readonly [8453];
31
+ };
32
+ export type AttestProfile = keyof typeof ATTEST_PROFILES;
33
+ export type AttestHex = `0x${string}`;
34
+ export type AttestCall = {
35
+ chainId: number;
36
+ to: string;
37
+ data: AttestHex;
38
+ value: '0';
39
+ fn: 'JevDecisionLog.record' | 'JevAnswerLog.record' | 'GradeStampRegistry.stamp';
40
+ };
41
+ export type AttestMissing = {
42
+ chainId: number;
43
+ network: string;
44
+ contract: 'JevDecisionLog' | 'JevAnswerLog' | 'GradeStampRegistry';
45
+ why: string;
46
+ };
47
+ /** the REF verdict vocabulary of GradeStampRegistry: 1 delivered as specified · 2 with defects · 3 not delivered · 4 cannot determine */
48
+ export type StampVerdict = 1 | 2 | 3 | 4;
49
+ export type AttestInput = {
50
+ /** the step: 'grade', 'assurance.settle', 'resale.<hop>' … (the decision log's kind is keccak256 of it) */
51
+ kind: string;
52
+ /** the job the step is about, on its own chain and contract */
53
+ subject: {
54
+ chainId: number;
55
+ contract: string;
56
+ jobId: string | number | bigint;
57
+ };
58
+ /** the canonical body of the step; its keccak256 is the digest unless `digest` is given */
59
+ body?: unknown | undefined;
60
+ digest?: AttestHex | undefined;
61
+ /** 0..10000; default 10000 (a deterministic step) */
62
+ confidenceBps?: number | undefined;
63
+ /** ≤ 64 bytes, e.g. 'jev-1.13.0' or 'det:assurance-settle-v1' */
64
+ model: string;
65
+ /** ≤ 512 bytes; default urn:taifoon:<kind>:<digest> */
66
+ uri?: string | undefined;
67
+ /** a stamp is built when there is a verdict; mode 1 deterministic (default) · 2 calibrated */
68
+ verdict?: StampVerdict | null | undefined;
69
+ mode?: 1 | 2 | undefined;
70
+ /** Jev's answer record digests: adds JevAnswerLog.record */
71
+ answers?: {
72
+ useCase: string;
73
+ inputDigest: AttestHex;
74
+ answersDigest: AttestHex;
75
+ uri?: string | undefined;
76
+ } | undefined;
77
+ };
78
+ export type AttestOptions = {
79
+ /** picks the default networks; default 'taifoon' */
80
+ profile?: AttestProfile | undefined;
81
+ /** overrides the profile: chain ids, or 'none' to opt out of recording (the mark is still returned) */
82
+ networks?: readonly number[] | 'none' | undefined;
83
+ /** extra or replacement contracts per chain (e.g. a fork's deploy, or a network the registry does not list) */
84
+ targets?: Readonly<Record<number, Partial<{
85
+ name: string;
86
+ decisionLog: string | null;
87
+ answerLog: string | null;
88
+ stampRegistry: string | null;
89
+ }>>> | undefined;
90
+ };
91
+ export type Attested = {
92
+ mark: {
93
+ kind: string;
94
+ kindId: AttestHex;
95
+ subject: AttestHex;
96
+ stampSubject: AttestHex;
97
+ digest: AttestHex;
98
+ confidenceBps: number;
99
+ verdict: StampVerdict | null;
100
+ model: string;
101
+ uri: string;
102
+ };
103
+ /** false only when the caller opted out */
104
+ recorded: boolean;
105
+ networks: number[];
106
+ calls: AttestCall[];
107
+ missing: AttestMissing[];
108
+ /** ready: every target contract exists · partial: some are missing · missing: none exist · none: opted out */
109
+ status: 'ready' | 'partial' | 'missing' | 'none';
110
+ };
111
+ export declare function attestKeccak(input: Uint8Array | string): AttestHex;
112
+ export declare const DECISION_LOG_RECORD_SIG = "record(bytes32,bytes32,bytes32,uint16,string,string)";
113
+ export declare const ANSWER_LOG_RECORD_SIG = "record(bytes32,bytes32,bytes32,bytes32,bytes32,string,string)";
114
+ export declare const STAMP_SIG = "stamp(bytes32,bytes32,uint8,uint8,string,string)";
115
+ /** canonical JSON: sorted keys, bigint as a decimal string, no whitespace (the digest's preimage) */
116
+ export declare function attestCanonical(v: unknown): string;
117
+ /** JevDecisionLog.subjectOf(chainId, at, ref) = keccak256(abi.encode("taifoon.decision.subject.v1", chainId, at, ref)) */
118
+ export declare const decisionSubjectOf: (chainId: number, at: string, ref: string | number | bigint) => AttestHex;
119
+ /** GradeStampRegistry.subjectOf(chainId, jobContract, jobId) = keccak256(abi.encode("taifoon.stamp.subject.v1", …)) */
120
+ export declare const stampSubjectOf: (chainId: number, jobContract: string, jobId: string | number | bigint) => AttestHex;
121
+ /** the stamp verdict of a settled ending, from the seller-record verdict the machine writes (endingOf): correct → 1;
122
+ * a Jev case (rejected or expired after delivery) → 4, it takes a grade to say more; incorrect → 3; excluded → no stamp */
123
+ export declare function stampVerdictOfEnding(e: {
124
+ verdict: string;
125
+ jevCase: boolean;
126
+ excluded: string | null;
127
+ }): StampVerdict | null;
128
+ /** the mark, the attestation calls and the stamp call of one step, on the profile's networks (or the ones named) */
129
+ export declare function attest(input: AttestInput, opts?: AttestOptions): Attested;
package/dist/attest.js ADDED
@@ -0,0 +1,225 @@
1
+ /**
2
+ * attest: the attested mark, the on-chain attestation and the stamp of one step (a grade, an assurance settle, a resale
3
+ * step), as UNSIGNED calls aimed at a default network chosen by profile. _ATTEST_BY_DEFAULT_v1_
4
+ *
5
+ * ONE file with NO imports, pure and browser-safe, like assurance.ts: @taifoon/jev, @taifoon/assurance (and through its
6
+ * sync, taifoon-sdk and moonbeam-sdk) carry a byte-identical copy and pin its hash. The addresses come from the address
7
+ * registry (packages/addresses) through scripts/gen-attest.mjs; nothing below the generated block names an address.
8
+ *
9
+ * mark what is attested, offline: the step's kind, the subject ids (decision log + stamp registry) and the
10
+ * digest = keccak256 of the canonical JSON body (or the digest the caller already has, e.g. Jev's).
11
+ * attestation JevDecisionLog.record(subject, kind, digest, confidenceBps, model, uri) on every target network, after
12
+ * JevAnswerLog.record(...) when the step carries Jev's answers.
13
+ * stamp GradeStampRegistry.stamp(subject, digest, verdict, mode, model, uri) when the step has a verdict.
14
+ *
15
+ * Recording is ON by default (the owner's rule of 2026-10-14): profile taifoon → the Taifoon devnet 36927 and the Taifoon
16
+ * mainnet 3692781 (GRID); profile moonbeam → Base 8453. Any network can be named instead; `networks: 'none'` opts out
17
+ * (the mark is still returned). A contract the registry does not have on a target network is listed in `missing`, with
18
+ * no call: nothing is faked. Nothing here signs or sends; the caller signs the calls.
19
+ */
20
+ // ── GENERATED from packages/addresses/addresses.json by scripts/gen-attest.mjs: do not edit ──
21
+ /** The attestation contracts on every chain the address registry has one on (superseded deploys left out). */
22
+ export const ATTEST_TARGETS = {
23
+ 143: { name: 'Monad', decisionLog: '0x1aC6266b9926cB3E206D5dc0c9d1B7c873aa150a', answerLog: '0x29fEEc116e0CAbbaf38e9EBF2BBf65d82D672104', stampRegistry: null },
24
+ 5042: { name: 'Arc', decisionLog: '0x863db7dAc69CC614a98822082cbe884F1f3b55cA', answerLog: '0x45214361A8cADb8aEe6302cd5653E119E5a89839', stampRegistry: null },
25
+ 8453: { name: 'Base', decisionLog: '0x209490d6A0FFC5368A42b0c2208BDCda853f6a92', answerLog: '0xC08dfc61d702AEA45d1EF2515d8BDA36d548C75c', stampRegistry: null },
26
+ 36927: { name: 'Taifoon devnet', decisionLog: '0x1D622511862DD7DEffB8fEeBc225E0FAdA1e9A05', answerLog: '0xD8c1d8188Fc8EE388792f6EB3B3dbd4f341Ba8e3', stampRegistry: '0xff5B4852AC7066D04FEd8de97bdA52401d03ad6E' },
27
+ 42161: { name: 'Arbitrum One', decisionLog: '0x1aC6266b9926cB3E206D5dc0c9d1B7c873aa150a', answerLog: '0x29fEEc116e0CAbbaf38e9EBF2BBf65d82D672104', stampRegistry: null },
28
+ 3692781: { name: 'Taifoon mainnet', decisionLog: '0xaEbF9084Bb32eB2E0F58537526A8877dd1436b23', answerLog: '0xC08dfc61d702AEA45d1EF2515d8BDA36d548C75c', stampRegistry: null },
29
+ };
30
+ // ── END GENERATED ──
31
+ /** The default networks per profile. Taifoon: its devnet and its mainnet (GRID). Moonbeam: Base only. */
32
+ export const ATTEST_PROFILES = { taifoon: [36927, 3692781], moonbeam: [8453] };
33
+ // ── keccak256 (the same code as packages/addresses/keccak.mjs; test/attest.test.ts checks known vectors) ──
34
+ const KM = (1n << 64n) - 1n;
35
+ const KRC = [
36
+ 0x0000000000000001n, 0x0000000000008082n, 0x800000000000808an, 0x8000000080008000n, 0x000000000000808bn, 0x0000000080000001n,
37
+ 0x8000000080008081n, 0x8000000000008009n, 0x000000000000008an, 0x0000000000000088n, 0x0000000080008009n, 0x000000008000000an,
38
+ 0x000000008000808bn, 0x800000000000008bn, 0x8000000000008089n, 0x8000000000008003n, 0x8000000000008002n, 0x8000000000000080n,
39
+ 0x000000000000800an, 0x800000008000000an, 0x8000000080008081n, 0x8000000000008080n, 0x0000000080000001n, 0x8000000080008008n,
40
+ ];
41
+ const KR = [0, 1, 62, 28, 27, 36, 44, 6, 55, 20, 3, 10, 43, 25, 39, 41, 45, 15, 21, 8, 18, 2, 61, 56, 14];
42
+ const krot = (v, n) => (n === 0 ? v : ((v << BigInt(n)) | (v >> BigInt(64 - n))) & KM);
43
+ function kf(A) {
44
+ const C = [0n, 0n, 0n, 0n, 0n];
45
+ const B = new Array(25).fill(0n);
46
+ const at = (a, i) => a[i];
47
+ for (let round = 0; round < 24; round++) {
48
+ for (let x = 0; x < 5; x++)
49
+ C[x] = at(A, x) ^ at(A, x + 5) ^ at(A, x + 10) ^ at(A, x + 15) ^ at(A, x + 20);
50
+ for (let x = 0; x < 5; x++) {
51
+ const d = at(C, (x + 4) % 5) ^ krot(at(C, (x + 1) % 5), 1);
52
+ for (let y = 0; y < 25; y += 5)
53
+ A[x + y] = at(A, x + y) ^ d;
54
+ }
55
+ for (let x = 0; x < 5; x++)
56
+ for (let y = 0; y < 5; y++)
57
+ B[y + 5 * ((2 * x + 3 * y) % 5)] = krot(at(A, x + 5 * y), KR[x + 5 * y]);
58
+ for (let x = 0; x < 5; x++)
59
+ for (let y = 0; y < 5; y++)
60
+ A[x + 5 * y] = at(B, x + 5 * y) ^ (~at(B, (x + 1) % 5 + 5 * y) & KM & at(B, (x + 2) % 5 + 5 * y));
61
+ A[0] = at(A, 0) ^ KRC[round];
62
+ }
63
+ }
64
+ /** UTF-8 bytes of a string (no TextEncoder: the file runs where no DOM or Node lib is typed) */
65
+ function utf8(s) {
66
+ const out = [];
67
+ for (const ch of s) {
68
+ const c = ch.codePointAt(0);
69
+ if (c < 0x80)
70
+ out.push(c);
71
+ else if (c < 0x800)
72
+ out.push(0xc0 | (c >> 6), 0x80 | (c & 63));
73
+ else if (c < 0x10000)
74
+ out.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 63), 0x80 | (c & 63));
75
+ else
76
+ out.push(0xf0 | (c >> 18), 0x80 | ((c >> 12) & 63), 0x80 | ((c >> 6) & 63), 0x80 | (c & 63));
77
+ }
78
+ return Uint8Array.from(out);
79
+ }
80
+ export function attestKeccak(input) {
81
+ const bytes = typeof input === 'string' ? utf8(input) : input;
82
+ const rate = 136;
83
+ const A = new Array(25).fill(0n);
84
+ const padded = new Uint8Array(Math.ceil((bytes.length + 1) / rate) * rate);
85
+ padded.set(bytes);
86
+ padded[bytes.length] = padded[bytes.length] ^ 0x01;
87
+ padded[padded.length - 1] = padded[padded.length - 1] ^ 0x80;
88
+ for (let off = 0; off < padded.length; off += rate) {
89
+ for (let i = 0; i < rate / 8; i++) {
90
+ let w = 0n;
91
+ for (let b = 7; b >= 0; b--)
92
+ w = (w << 8n) | BigInt(padded[off + i * 8 + b]);
93
+ A[i] = A[i] ^ w;
94
+ }
95
+ kf(A);
96
+ }
97
+ let out = '0x';
98
+ for (let i = 0; i < 4; i++)
99
+ for (let b = 0; b < 8; b++)
100
+ out += Number((A[i] >> BigInt(8 * b)) & 0xffn).toString(16).padStart(2, '0');
101
+ return out;
102
+ }
103
+ // ── ABI encoding (only the types these three calls use) ──
104
+ const hexOf = (b) => Array.from(b, (x) => x.toString(16).padStart(2, '0')).join('');
105
+ const bytesOf = (h) => { const s = h.replace(/^0x/, ''); const o = new Uint8Array(s.length / 2); for (let i = 0; i < o.length; i++)
106
+ o[i] = parseInt(s.slice(i * 2, i * 2 + 2), 16); return o; };
107
+ const word = (n) => { if (n < 0n || n >= 1n << 256n)
108
+ throw new Error(`attest: ${n} does not fit a uint256`); return n.toString(16).padStart(64, '0'); };
109
+ const isHex32 = (v) => /^0x[0-9a-fA-F]{64}$/.test(v);
110
+ const isAddr = (v) => /^0x[0-9a-fA-F]{40}$/.test(v);
111
+ function encodeArgs(args) {
112
+ let head = '';
113
+ let tail = '';
114
+ const headLen = args.length * 32;
115
+ for (const a of args) {
116
+ if (a.t === 'string') {
117
+ const b = utf8(String(a.v));
118
+ head += word(BigInt(headLen + tail.length / 2));
119
+ tail += word(BigInt(b.length)) + hexOf(b).padEnd(Math.ceil(b.length / 32) * 64, '0');
120
+ }
121
+ else if (a.t === 'bytes32') {
122
+ if (!isHex32(String(a.v)))
123
+ throw new Error(`attest: ${a.v} is not a bytes32`);
124
+ head += String(a.v).slice(2).toLowerCase();
125
+ }
126
+ else if (a.t === 'address') {
127
+ if (!isAddr(String(a.v)))
128
+ throw new Error(`attest: ${a.v} is not an address`);
129
+ head += String(a.v).slice(2).toLowerCase().padStart(64, '0');
130
+ }
131
+ else
132
+ head += word(BigInt(a.v));
133
+ }
134
+ return head + tail;
135
+ }
136
+ const selector = (sig) => attestKeccak(sig).slice(2, 10);
137
+ const call = (sig, args) => `0x${selector(sig)}${encodeArgs(args)}`;
138
+ export const DECISION_LOG_RECORD_SIG = 'record(bytes32,bytes32,bytes32,uint16,string,string)';
139
+ export const ANSWER_LOG_RECORD_SIG = 'record(bytes32,bytes32,bytes32,bytes32,bytes32,string,string)';
140
+ export const STAMP_SIG = 'stamp(bytes32,bytes32,uint8,uint8,string,string)';
141
+ /** canonical JSON: sorted keys, bigint as a decimal string, no whitespace (the digest's preimage) */
142
+ export function attestCanonical(v) {
143
+ if (typeof v === 'bigint')
144
+ return JSON.stringify(v.toString());
145
+ if (v === null || typeof v !== 'object')
146
+ return JSON.stringify(v ?? null);
147
+ if (Array.isArray(v))
148
+ return `[${v.map(attestCanonical).join(',')}]`;
149
+ const o = v;
150
+ return `{${Object.keys(o).filter((k) => o[k] !== undefined).sort().map((k) => `${JSON.stringify(k)}:${attestCanonical(o[k])}`).join(',')}}`;
151
+ }
152
+ const jobRef = (id) => (typeof id === 'string' && isHex32(id) ? id : `0x${word(BigInt(id))}`);
153
+ /** JevDecisionLog.subjectOf(chainId, at, ref) = keccak256(abi.encode("taifoon.decision.subject.v1", chainId, at, ref)) */
154
+ export const decisionSubjectOf = (chainId, at, ref) => attestKeccak(bytesOf(encodeArgs([{ t: 'string', v: 'taifoon.decision.subject.v1' }, { t: 'uint', v: chainId }, { t: 'address', v: at }, { t: 'bytes32', v: jobRef(ref) }])));
155
+ /** GradeStampRegistry.subjectOf(chainId, jobContract, jobId) = keccak256(abi.encode("taifoon.stamp.subject.v1", …)) */
156
+ export const stampSubjectOf = (chainId, jobContract, jobId) => attestKeccak(bytesOf(encodeArgs([{ t: 'string', v: 'taifoon.stamp.subject.v1' }, { t: 'uint', v: chainId }, { t: 'address', v: jobContract }, { t: 'bytes32', v: jobRef(jobId) }])));
157
+ /** the stamp verdict of a settled ending, from the seller-record verdict the machine writes (endingOf): correct → 1;
158
+ * a Jev case (rejected or expired after delivery) → 4, it takes a grade to say more; incorrect → 3; excluded → no stamp */
159
+ export function stampVerdictOfEnding(e) {
160
+ if (e.excluded)
161
+ return null;
162
+ if (e.verdict === 'correct')
163
+ return 1;
164
+ if (e.jevCase || e.verdict === 'undecided')
165
+ return 4;
166
+ if (e.verdict === 'incorrect')
167
+ return 3;
168
+ return null;
169
+ }
170
+ /** the mark, the attestation calls and the stamp call of one step, on the profile's networks (or the ones named) */
171
+ export function attest(input, opts = {}) {
172
+ const { subject } = input;
173
+ if (!isAddr(subject.contract))
174
+ throw new Error(`attest: subject.contract ${subject.contract} is not an address`);
175
+ if (!input.kind)
176
+ throw new Error('attest: a kind names the step');
177
+ const digest = input.digest ?? attestKeccak(attestCanonical(input.body ?? null));
178
+ if (!isHex32(digest) || /^0x0{64}$/.test(digest))
179
+ throw new Error('attest: the digest must be a non-zero bytes32');
180
+ const confidenceBps = input.confidenceBps ?? 10_000;
181
+ if (!Number.isInteger(confidenceBps) || confidenceBps < 0 || confidenceBps > 10_000)
182
+ throw new Error(`attest: confidenceBps ${confidenceBps} is not 0..10000`);
183
+ const model = input.model;
184
+ if (!model || utf8(model).length > 64)
185
+ throw new Error('attest: model is 1..64 bytes');
186
+ const uri = input.uri ?? `urn:taifoon:${input.kind}:${digest}`;
187
+ if (utf8(uri).length > 512)
188
+ throw new Error('attest: uri is ≤ 512 bytes');
189
+ const verdict = input.verdict ?? null;
190
+ const mode = input.mode ?? 1;
191
+ if (verdict !== null && ![1, 2, 3, 4].includes(verdict))
192
+ throw new Error(`attest: verdict ${verdict} is not 1..4`);
193
+ const kindId = attestKeccak(input.kind);
194
+ const mark = { kind: input.kind, kindId, subject: decisionSubjectOf(subject.chainId, subject.contract, subject.jobId), stampSubject: stampSubjectOf(subject.chainId, subject.contract, subject.jobId), digest, confidenceBps, verdict, model, uri };
195
+ if (opts.networks === 'none')
196
+ return { mark, recorded: false, networks: [], calls: [], missing: [], status: 'none' };
197
+ const networks = [...(opts.networks ?? ATTEST_PROFILES[opts.profile ?? 'taifoon'])];
198
+ const calls = [];
199
+ const missing = [];
200
+ for (const chainId of networks) {
201
+ // the defaults as their own value: written inline before a spread of a fully typed target, tsc refuses them (TS2783)
202
+ const unknownChain = { name: `chain ${chainId}`, decisionLog: null, answerLog: null, stampRegistry: null };
203
+ const t = { ...unknownChain, ...ATTEST_TARGETS[chainId], ...opts.targets?.[chainId] };
204
+ const gap = (contract) => missing.push({ chainId, network: t.name, contract, why: `no ${contract} on ${t.name} (${chainId}) in the address registry` });
205
+ // the answers first, then the decision that cites them (the order the layer's recorder sends)
206
+ if (input.answers) {
207
+ if (t.answerLog)
208
+ calls.push({ chainId, to: t.answerLog, value: '0', fn: 'JevAnswerLog.record', data: call(ANSWER_LOG_RECORD_SIG, [{ t: 'bytes32', v: attestKeccak(input.answers.useCase) }, { t: 'bytes32', v: mark.subject }, { t: 'bytes32', v: input.answers.inputDigest }, { t: 'bytes32', v: input.answers.answersDigest }, { t: 'bytes32', v: digest }, { t: 'string', v: model }, { t: 'string', v: input.answers.uri ?? uri }]) });
209
+ else
210
+ gap('JevAnswerLog');
211
+ }
212
+ if (t.decisionLog)
213
+ calls.push({ chainId, to: t.decisionLog, value: '0', fn: 'JevDecisionLog.record', data: call(DECISION_LOG_RECORD_SIG, [{ t: 'bytes32', v: mark.subject }, { t: 'bytes32', v: kindId }, { t: 'bytes32', v: digest }, { t: 'uint', v: confidenceBps }, { t: 'string', v: model }, { t: 'string', v: uri }]) });
214
+ else
215
+ gap('JevDecisionLog');
216
+ if (verdict !== null) {
217
+ if (t.stampRegistry)
218
+ calls.push({ chainId, to: t.stampRegistry, value: '0', fn: 'GradeStampRegistry.stamp', data: call(STAMP_SIG, [{ t: 'bytes32', v: mark.stampSubject }, { t: 'bytes32', v: digest }, { t: 'uint', v: verdict }, { t: 'uint', v: mode }, { t: 'string', v: model }, { t: 'string', v: uri }]) });
219
+ else
220
+ gap('GradeStampRegistry');
221
+ }
222
+ }
223
+ const status = missing.length === 0 ? 'ready' : calls.length === 0 ? 'missing' : 'partial';
224
+ return { mark, recorded: true, networks, calls, missing, status };
225
+ }
package/dist/chains.d.ts CHANGED
@@ -13,6 +13,10 @@ export declare const jevLogsOn: (chainId: number) => {
13
13
  address: string;
14
14
  fromBlock: number;
15
15
  };
16
+ earlierAnswerLogs?: readonly {
17
+ address: string;
18
+ fromBlock: number;
19
+ }[];
16
20
  } | null;
17
21
  /** Every chain that carries Jev logs, the devnet first, then Base, then the rest by id. */
18
22
  export declare const JEV_CHAINS: readonly number[];
package/dist/index.d.ts CHANGED
@@ -17,7 +17,8 @@
17
17
  export { grade } from './grade.js';
18
18
  export { facts } from './facts.js';
19
19
  export { prepareJob, runCheck } from './job.js';
20
- export { record } from './record.js';
20
+ export { record, STAMP_VERDICT_OF } from './record.js';
21
+ export { attest, ATTEST_PROFILES, ATTEST_TARGETS, decisionSubjectOf, stampSubjectOf } from './attest.js';
21
22
  export { evaluatorCall } from './evaluator/index.js';
22
23
  export { verify } from './verify.js';
23
24
  export { JevChainError, type JevChainErrorCode } from './errors.js';
@@ -31,6 +32,7 @@ export type { GradeInput, Receipt } from './grade.js';
31
32
  export type { FactsInput, Check } from './facts.js';
32
33
  export type { JobSpec, JobCheck, PreparedJob } from './job.js';
33
34
  export type { Recorded, UnsignedCall, Network } from './record.js';
35
+ export type { Attested, AttestInput, AttestOptions, AttestProfile, AttestMissing, AttestCall, StampVerdict } from './attest.js';
34
36
  export type { Verification, AnswerEvent, DecisionEvent, ChainStatus, VerifyOptions, VerifyNetwork } from './verify.js';
35
37
  export type { DecisionRecord, DecisionCheck } from './decision.js';
36
38
  export type { Protocol, Adapter, AdapterOpts, EvaluatorCall } from './evaluator/index.js';
package/dist/index.js CHANGED
@@ -17,7 +17,8 @@
17
17
  export { grade } from './grade.js';
18
18
  export { facts } from './facts.js';
19
19
  export { prepareJob, runCheck } from './job.js';
20
- export { record } from './record.js';
20
+ export { record, STAMP_VERDICT_OF } from './record.js';
21
+ export { attest, ATTEST_PROFILES, ATTEST_TARGETS, decisionSubjectOf, stampSubjectOf } from './attest.js';
21
22
  export { evaluatorCall } from './evaluator/index.js';
22
23
  export { verify } from './verify.js';
23
24
  export { JevChainError } from './errors.js';
@@ -108,10 +108,12 @@ export type PipelineOpts = {
108
108
  answers?: Record<string, Answer> | Answer[];
109
109
  /** the model that produced supplied `answers` (e.g. "jev-1.13.0"); it is part of the decision digest */
110
110
  model?: string | null;
111
- /** where record() points the calls; default devnet */
112
- /** recording is opt-in: where record() points the calls and what the layer may record. Default none: the receipt and
113
- * its digests only (recomputable offline), no transaction. */
111
+ /** where record() points the calls and what the layer may record. Recording is ON by default (_ATTEST_BY_DEFAULT_v1_):
112
+ * unsigned calls for the Taifoon devnet 36927 and the Taifoon mainnet 3692781 (a log a network lacks is listed as
113
+ * missing); `none` turns it off (the receipt and its digests only, recomputable offline). */
114
114
  network?: Network;
115
+ /** any chain ids instead of `network` (e.g. [8453] for Base) */
116
+ networks?: readonly number[];
115
117
  /** X-API-Key for POST /v1/judge/answers/record; omitted: nothing is written to the layer */
116
118
  relayerKey?: string | null;
117
119
  /** the job's protocol for the evaluator call; omitted: guessed from the job id */
package/dist/pipeline.js CHANGED
@@ -178,11 +178,11 @@ export async function pipeline(o = {}) {
178
178
  await step('record', async () => {
179
179
  if (!receipt.decision)
180
180
  return { skip: `nothing to record: ${receipt.reasons[0] ?? receipt.verdict} (the facts decided; Jev was not asked)` };
181
- t.recorded = await record(receipt, { network: o.network ?? 'none' });
181
+ t.recorded = await record(receipt, { network: o.network, networks: o.networks });
182
182
  if (layer && o.relayerKey && receipt.answersRecord)
183
- t.layerRecord = await getJson(f, `${layer}/v1/judge/answers/record`, { method: 'POST', headers: { 'content-type': 'application/json', 'x-api-key': o.relayerKey }, body: JSON.stringify({ ...receipt.answersRecord, record: o.network ?? 'none' }) });
183
+ t.layerRecord = await getJson(f, `${layer}/v1/judge/answers/record`, { method: 'POST', headers: { 'content-type': 'application/json', 'x-api-key': o.relayerKey }, body: JSON.stringify({ ...receipt.answersRecord, record: o.network ?? (o.networks ? (o.networks.includes(8453) ? (o.networks.includes(36927) ? 'both' : 'base') : o.networks.includes(36927) ? 'devnet' : 'none') : 'devnet') }) });
184
184
  const rec = t.layerRecord?.recording;
185
- return { status: t.recorded.status, recording: rec ? { used: rec.used ?? null, held: rec.held?.message ?? null } : null, calls: t.recorded.calls.map((c) => `${c.fn}@${c.chainId}${c.to ? '' : ' (no address)'}`), digests: t.recorded.digests, layer: t.layerRecord ?? (layer ? 'not written: pass a relayer key to record on the layer' : 'independent mode') };
185
+ return { status: t.recorded.status, recording: rec ? { used: rec.used ?? null, held: rec.held?.message ?? null } : null, calls: t.recorded.calls.map((c) => `${c.fn}@${c.chainId}`), missing: t.recorded.missing.map((m) => `${m.contract}@${m.chainId}`), digests: t.recorded.digests, layer: t.layerRecord ?? (layer ? 'not written: pass a relayer key to record on the layer' : 'independent mode') };
186
186
  });
187
187
  await step('evaluator', async () => {
188
188
  if (receipt.verdict === 'needs_review')
package/dist/record.d.ts CHANGED
@@ -1,5 +1,7 @@
1
- import { type Hex } from './hash.js';
1
+ import { type AttestMissing, type AttestProfile, type StampVerdict } from './attest.js';
2
+ import type { Hex } from './hash.js';
2
3
  import type { Receipt } from './receipt.js';
4
+ import type { Verdict } from './rubric.js';
3
5
  export type Network = 'none' | 'devnet' | 'base' | 'both';
4
6
  export type UnsignedCall = {
5
7
  chainId: number;
@@ -9,24 +11,35 @@ export type UnsignedCall = {
9
11
  fn: string;
10
12
  };
11
13
  export type Recorded = {
12
- network: Network;
13
- /** ready: every call has an address · partial: some have `to: null` · sent: `send` sent the addressed ones · none: nothing to send */
14
- status: 'ready' | 'partial' | 'sent' | 'none';
14
+ network: Network | 'default' | 'chains';
15
+ /** the chains the calls aim at (empty when opted out) */
16
+ networks: number[];
17
+ /** ready: every contract exists · partial: some are missing · missing: none exist · sent: `send` sent the calls · none: opted out */
18
+ status: 'ready' | 'partial' | 'missing' | 'sent' | 'none';
15
19
  calls: UnsignedCall[];
20
+ /** a contract a target network does not have: said here, with no call */
21
+ missing: AttestMissing[];
16
22
  digests: {
17
23
  answers: Hex;
18
24
  decision: Hex;
19
25
  input: Hex;
20
26
  subject: Hex;
21
27
  receipt: Hex;
28
+ stampSubject: Hex;
22
29
  };
23
30
  notes: string[];
24
31
  sent?: string[];
25
32
  };
26
33
  export declare const ANSWER_LOG_RECORD = "record(bytes32,bytes32,bytes32,bytes32,bytes32,string,string)";
27
34
  export declare const DECISION_LOG_RECORD = "record(bytes32,bytes32,bytes32,uint16,string,string)";
35
+ /** the composed verdict → the stamp's REF code. Jev is asked only when the facts found a delivery, so a reject is a
36
+ * delivery with defects (2), not a missing one; needs_review cannot be determined yet (4). */
37
+ export declare const STAMP_VERDICT_OF: Record<Verdict, StampVerdict>;
28
38
  export declare function record(r: Receipt, opts?: {
29
39
  network?: Network;
40
+ networks?: readonly number[];
41
+ profile?: AttestProfile;
30
42
  uri?: string;
43
+ stamp?: boolean;
31
44
  send?: (call: UnsignedCall) => Promise<string>;
32
45
  }): Promise<Recorded>;
package/dist/record.js CHANGED
@@ -1,39 +1,47 @@
1
- // record(): the unsigned calls that put a receipt's digests on chain — JevAnswerLog.record (jev.answer.v1) and
2
- // JevDecisionLog.record (decision.v2). `network` is a flag: none | devnet | base | both. Nothing here signs: pass `send`
3
- // (your wallet, e.g. viem's walletClient.sendTransaction) to send the calls that have an address, or hand them on.
4
- import { encodeCall } from './abi.js';
5
- import { CONTRACTS } from './contracts.js';
6
- import { keccakHex } from './hash.js';
1
+ // record(): the unsigned calls that put a receipt on chain — JevAnswerLog.record (jev.answer.v1), JevDecisionLog.record
2
+ // (decision.v2) and GradeStampRegistry.stamp (the grade's verdict) — built by the shared attestation helper (attest.ts,
3
+ // vendored byte for byte). Recording is ON by default (_ATTEST_BY_DEFAULT_v1_): the Taifoon devnet 36927 and the Taifoon
4
+ // mainnet 3692781. `network` still takes none | devnet | base | both; `networks` takes any chain ids. A log or registry a
5
+ // network does not have is listed in `missing`, never faked. Nothing here signs: pass `send` (your wallet, e.g. viem's
6
+ // walletClient.sendTransaction) to send the calls, or hand them on.
7
+ import { attest, ATTEST_PROFILES } from './attest.js';
8
+ import { refBytes32, ZERO_ADDRESS } from './records.js';
7
9
  export const ANSWER_LOG_RECORD = 'record(bytes32,bytes32,bytes32,bytes32,bytes32,string,string)';
8
10
  export const DECISION_LOG_RECORD = 'record(bytes32,bytes32,bytes32,uint16,string,string)';
9
- const TARGETS = {
10
- devnet: () => ({ chainId: CONTRACTS.devnet.chainId, answerLog: CONTRACTS.devnet.answerLog.address, decisionLog: CONTRACTS.devnet.decisionLog.address,
11
- note: 'devnet 36927: any account may send (gas from https://faucet.taifoon.dev). A sender outside JevAnswerLog’s deploy-time recorders is logged with trusted = false.' }),
12
- base: () => ({ chainId: CONTRACTS.base.chainId, answerLog: CONTRACTS.base.answerLog?.address ?? null, decisionLog: CONTRACTS.base.decisionLog?.address ?? null,
13
- note: CONTRACTS.base.answerLog && CONTRACTS.base.decisionLog ? 'Base 8453: the logs at CONTRACTS.base.' : 'Base 8453: this package version carries no Base address for the logs, so these calls have to = null (the call data is final; set `to` to send them).' }),
14
- };
11
+ /** the composed verdict → the stamp's REF code. Jev is asked only when the facts found a delivery, so a reject is a
12
+ * delivery with defects (2), not a missing one; needs_review cannot be determined yet (4). */
13
+ export const STAMP_VERDICT_OF = { complete: 1, reject: 2, needs_review: 4 };
14
+ const NETWORKS = { devnet: [36927], base: [8453], both: [36927, 8453] };
15
15
  export async function record(r, opts = {}) {
16
16
  if (!r.decision || !r.answersRecord || !r.answersDigest)
17
17
  throw new Error(`nothing to record: Jev was not asked (${r.reasons[0] ?? r.verdict}) — a hard fail is decided by the facts alone`);
18
- const network = opts.network ?? 'devnet';
19
- const digests = { answers: r.answersDigest, decision: r.decision.digest, input: r.inputDigest, subject: r.subjectId, receipt: r.receiptHash };
20
- if (network === 'none')
21
- return { network, status: 'none', calls: [], digests, notes: ['network none: the receipt and its digests only'] };
18
+ const network = opts.network ?? (opts.networks ? 'chains' : 'default');
19
+ const chains = opts.network === 'none' ? 'none' : opts.network ? NETWORKS[opts.network] : opts.networks ?? ATTEST_PROFILES[opts.profile ?? 'taifoon'];
22
20
  const model = (r.answersRecord.upstream_model ?? r.answersRecord.model ?? 'jev').slice(0, 64);
23
21
  const uri = (kind, d) => (opts.uri ? `${opts.uri.replace(/\/$/, '')}/${d}` : `urn:jev:${kind}:${d}`).slice(0, 512);
24
- const answerData = encodeCall(ANSWER_LOG_RECORD, [keccakHex(r.answersRecord.use_case), r.subjectId, r.inputDigest, r.answersDigest, r.decision.digest, model, uri('answers', r.answersDigest)]);
25
- const decisionData = encodeCall(DECISION_LOG_RECORD, [r.subjectId, r.decision.kindId, r.decision.digest, r.decision.confidenceBps, model, uri('decision', r.decision.digest)]);
26
- const targets = (network === 'both' ? ['devnet', 'base'] : [network]).map((n) => TARGETS[n]());
27
- const calls = targets.flatMap((t) => [
28
- { chainId: t.chainId, to: t.answerLog, value: '0', fn: 'JevAnswerLog.record', data: answerData },
29
- { chainId: t.chainId, to: t.decisionLog, value: '0', fn: 'JevDecisionLog.record', data: decisionData },
30
- ]);
31
- const out = { network, status: calls.every((c) => c.to) ? 'ready' : 'partial', calls, digests, notes: targets.map((t) => t.note) };
22
+ const s = r.chainSubject;
23
+ const a = attest({
24
+ kind: r.decision.kind, subject: { chainId: s.chainId, contract: s.at ?? ZERO_ADDRESS, jobId: refBytes32(s.ref) },
25
+ digest: r.decision.digest, confidenceBps: r.decision.confidenceBps, model, uri: uri('decision', r.decision.digest),
26
+ answers: { useCase: r.answersRecord.use_case, inputDigest: r.inputDigest, answersDigest: r.answersDigest, uri: uri('answers', r.answersDigest) },
27
+ verdict: opts.stamp === false ? null : STAMP_VERDICT_OF[r.verdict], mode: 2,
28
+ }, { networks: chains });
29
+ if (a.mark.subject !== r.subjectId)
30
+ throw new Error('record: the attestation subject differs from the receipt\'s subject id');
31
+ const digests = { answers: r.answersDigest, decision: r.decision.digest, input: r.inputDigest, subject: r.subjectId, receipt: r.receiptHash, stampSubject: a.mark.stampSubject };
32
+ if (!a.recorded)
33
+ return { network, networks: [], status: 'none', calls: [], missing: [], digests, notes: ['recording turned off (network none): the receipt and its digests only'] };
34
+ const out = {
35
+ network, networks: a.networks, status: a.status === 'none' ? 'none' : a.status, calls: a.calls, missing: a.missing, digests,
36
+ notes: [
37
+ ...(a.networks.includes(36927) ? ['devnet 36927: any account may send (gas from https://faucet.taifoon.dev). A sender outside JevAnswerLog’s deploy-time recorders is logged with trusted = false.'] : []),
38
+ ...a.missing.map((m) => m.why),
39
+ ],
40
+ };
32
41
  if (opts.send) {
33
42
  out.sent = [];
34
- for (const c of calls)
35
- if (c.to)
36
- out.sent.push(await opts.send(c));
43
+ for (const c of out.calls)
44
+ out.sent.push(await opts.send(c));
37
45
  if (out.sent.length)
38
46
  out.status = 'sent';
39
47
  }
@@ -1,4 +1,4 @@
1
- // GENERATED by scripts/vendor-rpc.mjs from packages/rpc/fallback.json (measured 2026-10-08). Do not edit.
1
+ // GENERATED by scripts/vendor-rpc.mjs from packages/rpc/fallback.json (measured 2026-10-10). Do not edit.
2
2
  // The fallback only: verify() asks warmbed's rotation first and uses these when warmbed does not answer.
3
3
  export const RPC_FALLBACK = {
4
4
  36927: {
package/dist/verify.js CHANGED
@@ -15,6 +15,33 @@ const hexN = (n) => '0x' + n.toString(16);
15
15
  async function logs(rpc, f, address, topics, fromBlock, toBlock, chainId) {
16
16
  return (await rpcCall(rpc, f, 'eth_getLogs', [{ address, topics, fromBlock: hexN(fromBlock), toBlock: toBlock == null ? 'latest' : hexN(toBlock) }], { chainId })) ?? [];
17
17
  }
18
+ /**
19
+ * _LOGS_AT_TRUSTED_v1_: the rows of the one block JevAnswerLog.recordedAt named. That block holds a trusted record, so an
20
+ * empty answer is the endpoint's (one that serves only recent logs answers [] for an older block instead of an error:
21
+ * base.rpc.blxrbdn.com, first in Base's rotation, did for a 1.35M-block-old record on 2026-10-29) and the next endpoint
22
+ * is asked. [] only when every endpoint that answered said so; no answer at all is rpc_unavailable, as before.
23
+ */
24
+ async function logsAtRecorded(rpc, f, address, topics, block, chainId) {
25
+ const urls = typeof rpc === 'string' ? [rpc] : rpc;
26
+ let answered = false;
27
+ let last = null;
28
+ for (const u of urls) {
29
+ try {
30
+ const l = await logs(u, f, address, topics, block, block, chainId);
31
+ answered = true;
32
+ if (l.length)
33
+ return l;
34
+ }
35
+ catch (e) {
36
+ if (e instanceof JevChainError && e.code !== 'rpc_unavailable')
37
+ throw e;
38
+ last = e;
39
+ }
40
+ }
41
+ if (!answered && last)
42
+ throw last;
43
+ return [];
44
+ }
18
45
  const SEL_RECORDED_AT = keccakHex('recordedAt(bytes32)').slice(0, 10);
19
46
  /** JevAnswerLog.recordedAt(digest): the block of the first trusted record, 0 when none. */
20
47
  async function recordedAt(rpc, f, address, digest, chainId) {
@@ -125,9 +152,14 @@ export async function verify(x, opts = {}) {
125
152
  const given = typeof opts.rpc === 'string' ? opts.rpc.split(',').map((u) => u.trim()).filter(Boolean) : opts.rpc;
126
153
  const rpc = given?.length ? given : await rpcRotation(chainId, f);
127
154
  if (chainId !== DEVNET) {
128
- const at = await recordedAt(rpc, f, dev.answerLog.address, answersDigest, chainId);
129
- if (at > 0)
130
- onChain.answers = (await logs(rpc, f, dev.answerLog.address, [TOPIC_ANSWERED, null, null, answersDigest], at, at, chainId)).map(answerEvent);
155
+ // _JEV_SEATS_v1_: the answer log records go to (JevAnswerLogV2 where live), then the earlier ones still read (Base: the first log)
156
+ for (const log of [dev.answerLog, ...(dev.earlierAnswerLogs ?? [])]) {
157
+ const at = await recordedAt(rpc, f, log.address, answersDigest, chainId);
158
+ if (at > 0) {
159
+ onChain.answers = (await logsAtRecorded(rpc, f, log.address, [TOPIC_ANSWERED, null, null, answersDigest], at, chainId)).map(answerEvent);
160
+ break;
161
+ }
162
+ }
131
163
  }
132
164
  else
133
165
  onChain.answers = (await logs(rpc, f, dev.answerLog.address, [TOPIC_ANSWERED, null, null, answersDigest], dev.answerLog.fromBlock, undefined, chainId)).map(answerEvent);
package/docs/API.md CHANGED
@@ -6,7 +6,7 @@
6
6
  |---|---|---|
7
7
  | `grade({ subject, evidence, facts?, rubric?, key \| answers })` | Checks the facts first. If none fails, asks Jev the rubric, composes the verdict and returns the receipt. | Asks Jev once, or never on a hard fail |
8
8
  | `facts({ delivered, checks, priceUsdc? })` | Runs the checks your protocol supplies. A check is a value or an (async) function; one that throws counts as "could not be checked". | None |
9
- | `record(receipt, { network, send? })` | Returns the unsigned `JevAnswerLog.record` and `JevDecisionLog.record` calls. `network` is a flag: `none`, `devnet` (default), `base` or `both`. Pass `send` (your wallet) to send every call that has an address. | None unless you pass `send` |
9
+ | `record(receipt, { network?, networks?, stamp?, send? })` | Returns the unsigned `JevAnswerLog.record`, `JevDecisionLog.record` and `GradeStampRegistry.stamp` calls. By default they aim at the Taifoon devnet 36927 and the Taifoon mainnet 3692781; `network` (`none`, `devnet`, `base`, `both`) or `networks` (any chain ids) choose others, `none` turns recording off. A contract a network does not have is listed in `missing`, with no call. Pass `send` (your wallet) to send the calls. | None unless you pass `send` |
10
10
  | `evaluatorCall(protocol, jobId, verdict, digest)` | Returns the one unsigned call that ends the job on that protocol. For `needs_review` it returns `null`. | None |
11
11
  | `prepareJob({ id, task, criteria, delivered, source?, checks? })` | Turns a job as the buyer laid it out into the evidence Jev reads and the checks code runs. | None |
12
12
  | `verify(receiptOrDigest)` | Recomputes every digest, re-composes the verdict and finds the `JevAnswered` and `Decided` events. | One devnet RPC read |
package/docs/PIPELINE.md CHANGED
@@ -12,8 +12,8 @@ This package puts Jev inside a fixed judging procedure:
12
12
  - Code establishes the facts first. A failed check ends the job, and Jev is never asked.
13
13
  - Jev answers four **atomic** questions from a published rubric. It never gets a holistic "is the job good?".
14
14
  - Code composes the verdict from the facts and the answers under published thresholds.
15
- - Everything is hashed into a receipt and turned into the one call that ends the job. Recording it on chain is
16
- optional: `record()` builds the calls only when you ask.
15
+ - Everything is hashed into a receipt and turned into the one call that ends the job. `record()` builds the
16
+ unsigned calls that record and stamp it on chain, on the Taifoon devnet and mainnet by default (`network: 'none'` turns them off).
17
17
 
18
18
  How to describe the result: *graded against a published rubric by a pinned decision model, with an appeal.* It is not
19
19
  "independently verified". An on-chain record is an attestation by whoever sent it, not a proof that the answer is right.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@taifoon/jev",
3
- "version": "0.2.18",
4
- "description": "Grade AI agent jobs with TypeSafe's Jev and record the verdict on chain: code checks the facts, Jev answers a published rubric, and you get a receipt, the on-chain record and the evaluator call for ERC-8183 and Virtuals ACP. No runtime dependencies, no key inside.",
3
+ "version": "0.4.0",
4
+ "description": "Grade AI agent jobs with TypeSafe's Jev and record the verdict on chain: code checks the facts, Jev answers a published rubric, and you get a receipt, the on-chain record and the evaluator call for ERC-8183 and Virtuals ACP. No third-party runtime dependencies, no key inside.",
5
5
  "keywords": [
6
6
  "jev",
7
7
  "typesafe",
@@ -18,7 +18,16 @@
18
18
  "ai-agents",
19
19
  "on-chain",
20
20
  "attestation",
21
- "base"
21
+ "base",
22
+ "taifoon",
23
+ "agent-evaluation",
24
+ "ai-evaluation",
25
+ "llm-evaluation",
26
+ "verifiable-ai",
27
+ "receipt",
28
+ "x402",
29
+ "arbitrum",
30
+ "monad"
22
31
  ],
23
32
  "license": "MIT",
24
33
  "author": {
@@ -51,6 +60,7 @@
51
60
  "examples",
52
61
  "README.md",
53
62
  "LICENSE",
63
+ "CHANGELOG.md",
54
64
  "!docs/demo.gif"
55
65
  ],
56
66
  "sideEffects": false,