@taifoon/jev 0.1.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.
Files changed (56) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +222 -0
  3. package/bin/jev.mjs +37 -0
  4. package/dist/abi.d.ts +11 -0
  5. package/dist/abi.js +81 -0
  6. package/dist/ask.d.ts +27 -0
  7. package/dist/ask.js +47 -0
  8. package/dist/contracts.d.ts +37 -0
  9. package/dist/contracts.js +27 -0
  10. package/dist/evaluator/assurance-hook.d.ts +6 -0
  11. package/dist/evaluator/assurance-hook.js +15 -0
  12. package/dist/evaluator/bitagent-erc8183.d.ts +7 -0
  13. package/dist/evaluator/bitagent-erc8183.js +18 -0
  14. package/dist/evaluator/index.d.ts +12 -0
  15. package/dist/evaluator/index.js +20 -0
  16. package/dist/evaluator/judge-adapter.d.ts +5 -0
  17. package/dist/evaluator/judge-adapter.js +16 -0
  18. package/dist/evaluator/types.d.ts +27 -0
  19. package/dist/evaluator/types.js +6 -0
  20. package/dist/evaluator/virtuals-erc8183.d.ts +6 -0
  21. package/dist/evaluator/virtuals-erc8183.js +15 -0
  22. package/dist/evaluator/virtuals-memo-acp.d.ts +5 -0
  23. package/dist/evaluator/virtuals-memo-acp.js +15 -0
  24. package/dist/facts.d.ts +16 -0
  25. package/dist/facts.js +15 -0
  26. package/dist/grade.d.ts +32 -0
  27. package/dist/grade.js +33 -0
  28. package/dist/hash.d.ts +10 -0
  29. package/dist/hash.js +132 -0
  30. package/dist/index.d.ts +29 -0
  31. package/dist/index.js +20 -0
  32. package/dist/receipt.d.ts +68 -0
  33. package/dist/receipt.js +42 -0
  34. package/dist/record.d.ts +32 -0
  35. package/dist/record.js +41 -0
  36. package/dist/records.d.ts +78 -0
  37. package/dist/records.js +55 -0
  38. package/dist/rubric.d.ts +109 -0
  39. package/dist/rubric.js +91 -0
  40. package/dist/verify.d.ts +40 -0
  41. package/dist/verify.js +80 -0
  42. package/package.json +73 -0
  43. package/workflows/0.1.0/README.md +104 -0
  44. package/workflows/0.1.0/batch-judge.json +341 -0
  45. package/workflows/0.1.0/hire-judge-settle.json +767 -0
  46. package/workflows/0.1.0/jev-grader.json +433 -0
  47. package/workflows/0.1.0/manifest.json +151 -0
  48. package/workflows/0.1.0/schemas/answers.json +248 -0
  49. package/workflows/0.1.0/schemas/common.json +394 -0
  50. package/workflows/0.1.0/schemas/delivery.json +233 -0
  51. package/workflows/0.1.0/schemas/facts.json +225 -0
  52. package/workflows/0.1.0/schemas/hire.json +337 -0
  53. package/workflows/0.1.0/schemas/job.json +96 -0
  54. package/workflows/0.1.0/schemas/settlement.json +225 -0
  55. package/workflows/0.1.0/schemas/verdict.json +314 -0
  56. package/workflows/0.1.0/stamp-grade.json +175 -0
package/LICENSE ADDED
@@ -0,0 +1,18 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Taifoon
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
6
+ associated documentation files (the "Software"), to deal in the Software without restriction,
7
+ including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
8
+ and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
9
+ subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all copies or substantial
12
+ portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
15
+ LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
16
+ NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
17
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
18
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,222 @@
1
+ # @taifoon/jev
2
+
3
+ Jev is a grader that any protocol can call from a contract's evaluator seat. The pipeline is code facts, then calibrated
4
+ answers, then a composed verdict, a receipt, the on-chain record and finally the evaluator call.
5
+
6
+ Built by Taifoon. MIT. The package has no runtime dependencies and contains no key.
7
+
8
+ ```
9
+ npm i @taifoon/jev
10
+ ```
11
+
12
+ The package also ships the n8n workflows that run Jev as a versioned bundle (see [Workflows](#workflows)).
13
+
14
+ ## What Jev is
15
+
16
+ Jev is TypeSafe's System One decision model. When this was written it answered as **`jev-1.13.0`**. You don't ask Jev
17
+ for prose. You give it closed questions, and every answer comes back as a **full probability distribution** over the
18
+ options, plus a confidence value. An answer to "did the delivery meet the spec?" looks like `{ yes: 0.93, no: 0.07 }`,
19
+ not a paragraph.
20
+
21
+ This package puts Jev inside a fixed judging procedure:
22
+
23
+ - Code establishes the facts first. A failed check ends the job, and Jev is never asked.
24
+ - Jev answers four **atomic** questions from a published rubric. It never gets a holistic "is the job good?".
25
+ - Code composes the verdict from the facts and the answers under published thresholds.
26
+ - Everything is hashed into a receipt, recorded on chain and turned into the one call that ends the job.
27
+
28
+ How to describe the result: *graded against a published rubric by a pinned decision model, with an appeal.* It is not
29
+ "independently verified". An on-chain record is an attestation by whoever sent it, not a proof that the answer is right.
30
+
31
+ ## The pipeline
32
+
33
+ ```
34
+ evidence ─► facts() ──── a check failed? ── yes ─► reject (hard fail; Jev is not asked)
35
+ code checks │
36
+ no
37
+ ▼
38
+ Jev reads: evidence (cut at 3,600 chars) + the facts section + one instruction
39
+ Jev answers RUBRIC_v1: spec_met · unsupported_claim · ending · cheat_shaped
40
+ ▼
41
+ compose under THRESHOLDS_v1 ─► complete | reject | needs_review
42
+ ▼
43
+ receipt: rubric hash, input digest, every distribution, verdict, reasons, receiptHash
44
+ ▼
45
+ record() ─► JevAnswerLog.record (jev.answer.v1) + JevDecisionLog.record (decision.v2)
46
+ ▼
47
+ evaluatorCall() ─► complete / reject on the job's protocol (needs_review ends nothing: appeal)
48
+ ```
49
+
50
+ THRESHOLDS_v1:
51
+
52
+ - **complete** needs spec_met ≥ 0.85, unsupported_claim ≤ 0.20 and ending = complete.
53
+ - **reject** follows from spec_met ≤ 0.40, unsupported_claim ≥ 0.70 or ending = reject.
54
+ - **needs_review** is the result when cheat_shaped ≥ 0.50 (escalate, never slash on this alone) or when the answers
55
+ land in the mid band.
56
+ - Above 50 USDC a complete is held for review.
57
+
58
+ ## Quickstart
59
+
60
+ This is `examples/quickstart.mjs`, run as is:
61
+
62
+ ```js
63
+ import { createHash } from 'node:crypto';
64
+ import { grade, facts, record, evaluatorCall, verify } from '@taifoon/jev';
65
+
66
+ const task = 'Return the SHA-256 hex digest of the ASCII string "jev".';
67
+ const delivered = createHash('sha256').update('jev').digest('hex'); // what the seller handed in
68
+
69
+ const receipt = await grade({
70
+ subject: { chainId: 8453, ref: 'quickstart:sha256-jev' }, // what is being judged (your job id)
71
+ evidence: { task, delivered },
72
+ facts: facts({ delivered: true, checks: { digest_recomputes: () => createHash('sha256').update('jev').digest('hex') === delivered } }),
73
+ trial: true, // 3 free calls; or { key: process.env.TYPESAFE_KEY }
74
+ });
75
+ console.log(receipt.verdict, '·', receipt.reasons[0]);
76
+ console.log('spec_met', receipt.answers?.spec_met.probabilities, '· model', receipt.model);
77
+ const onchain = await record(receipt); // unsigned: JevAnswerLog + JevDecisionLog (devnet)
78
+ const end = evaluatorCall('virtuals-erc8183', 1n, receipt, receipt); // unsigned: complete / reject as job 1's evaluator
79
+ console.log(onchain.calls.map((c) => c.fn), '→', end?.fn, '· verifies:', (await verify(receipt, { chain: false })).ok);
80
+ ```
81
+
82
+ Real output from one call to the public trial (2026-09-27):
83
+
84
+ ```
85
+ complete · spec_met 0.93 ≥ 0.85 · unsupported_claim 0.10 ≤ 0.2 · ending complete
86
+ spec_met { no: 0.07, yes: 0.93 } · model jev-1.13.0
87
+ [ 'JevAnswerLog.record', 'JevDecisionLog.record' ] → complete(uint256,bytes32,bytes) · verifies: true
88
+ ```
89
+
90
+ ## The five calls
91
+
92
+ | Call | What it does | Network |
93
+ |---|---|---|
94
+ | `grade({ subject, evidence, facts?, rubric?, key? \| trial })` | 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 |
95
+ | `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 |
96
+ | `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` |
97
+ | `evaluatorCall(protocol, jobId, verdict, digest)` | Returns the one unsigned call that ends the job on that protocol. For `needs_review` it returns `null`. | None |
98
+ | `verify(receiptOrDigest)` | Recomputes every digest, re-composes the verdict and finds the `JevAnswered` and `Decided` events. | One devnet RPC read |
99
+
100
+ It also exports `RUBRIC_v1` and `CONTRACTS`. Nothing else.
101
+
102
+ **Where Jev is reached.** With `trial: true`, calls go to `https://typesafe.taifoon.dev/v1/trial`. The trial is free for
103
+ 3 calls per caller, takes at most 4 questions and 4,000 JSON characters, and SDK cuts the evidence to fit (never the facts
104
+ section). With `key`, calls go to `https://api.typesafe.ai/v1/systemone` using your own TypeSafe key from
105
+ [console.typesafe.ai](https://console.typesafe.ai). The key is not stored and never appears in a receipt. If you already
106
+ have the answers (from the n8n node, for example), pass `answers` and nothing is asked.
107
+
108
+ **Your own rubric.** `grade({ rubric: { version, questions, asked?, thresholds?, compose? } })`. The rubric hash is
109
+ sha256 of its questions and goes into the receipt.
110
+
111
+ ## Evaluator seats
112
+
113
+ Each adapter is one small file with its ABI fragment. Each one is tested against calldata that a mined transaction
114
+ carried (`test/fixtures/evaluator-calls.json`, written by `scripts/record-evaluator-fixtures.mjs`).
115
+
116
+ | `protocol` | Call | Tested against |
117
+ |---|---|---|
118
+ | `assurance-hook` | `complete(bytes32)` / `reject(bytes32 jobId, bytes32 digest)`, with `opts.to` = the hook | Base [`0xc606d00f…3116`](https://basescan.org/tx/0xc606d00fed64e912585bbc93feb15a9289cfcde94289f49f7c74a7a468903116), the complete of a covered GLMR hire on Moonbeam's hook (funded in [`0xfbe436ce…f193`](https://basescan.org/tx/0xfbe436ced5caf66dde0e96033f041480316fc5f62cd273006ad9c1852a57f193)) |
119
+ | `judge-adapter` | `postVerdict(bytes32 jobId, 1\|2, 2, bytes32 digest)` → hook complete / reject | devnet [`0x07853069…2012`](https://www.taifoon.io/scan/36927/tx/0x07853069cf43200fad3761d05b532c654df6f02b8a47f5f767bec18cb91a2012) |
120
+ | `virtuals-erc8183` | AgenticCommerceV3 `complete` / `reject(uint256, bytes32 reason, bytes)` as the job's evaluator | Base [`0x60890802…27bd`](https://basescan.org/tx/0x60890802a3b399af7face54c1cb33d771cffc9b8594d0113de14ae290b5c27bd) (complete) and [`0x152b5fb8…db76`](https://basescan.org/tx/0x152b5fb8900e57119cee6cbc15206d8938c11dc4056b20ec4c986f4d8acddb76) (reject), inside ERC-4337 handleOps |
121
+ | `virtuals-memo-acp` | ACPRouter `signMemo(uint256 memoId, bool approved, string reason)` on the evaluation memo | Base [`0x94ba6b49…622e`](https://basescan.org/tx/0x94ba6b497e2fc2ecf7ae256e1a490cabdc4a5357ed306ee2071f46a1c4ef622e) |
122
+ | `bitagent-erc8183` | AgenticCommerce `complete` / `reject(uint256, bytes32, bytes)` as the job's evaluator; `seat: 'platform'` → Evaluator `forceSettleJob(uint256, bool)` | Base [`0x195359ba…b01e`](https://basescan.org/tx/0x195359ba64f8ba429c449e82c27d0c57486511593646886b59a0d80c808fb01e) (forceSettleJob, job 8941) and its submit [`0xa4a49696…4ebc`](https://basescan.org/tx/0xa4a496968877ccbf1ba73fe5c5908f064d7333a2ced984e1867da9ee70f74ebc) (same layout) |
123
+
124
+ The digest in the call is the receipt's decision digest. That is the same 32 bytes JevDecisionLog holds, so the job's
125
+ ending, the decision record and the receipt all carry one value. On a hard fail no decision exists, so pass
126
+ `receipt.receiptHash`.
127
+
128
+ Whoever holds the seat signs the call; this package never does. The BitAgent implementation is unverified on Base, and
129
+ its layout was read from its own transactions. Its jobs settle today through the platform Evaluator, which only
130
+ BitAgent's operators can call.
131
+
132
+ ## On chain today
133
+
134
+ The logs are on the public devnet (chain 36927, free gas from `https://faucet.taifoon.dev`). Any account may send to
135
+ them.
136
+
137
+ | Contract | Address | Rule |
138
+ |---|---|---|
139
+ | JevAnswerLog | `0xD8c1d8188Fc8EE388792f6EB3B3dbd4f341Ba8e3` | Append-only, no owner. Recorders fixed at deploy are `trusted`; any other sender is logged with `trusted = false` |
140
+ | JevDecisionLog | `0x1D622511862DD7DEffB8fEeBc225E0FAdA1e9A05` | Append-only, no owner, no upgrade. The recorder is `msg.sender` |
141
+
142
+ For Base, `CONTRACTS.base.answerLog` and `CONTRACTS.base.decisionLog` are `null` in 0.1.0. So `record(receipt, { network: 'base' })` returns the Base call data with `to: null` and `status: 'partial'`, and `send` skips those calls.
143
+
144
+ Here is one real grade, end to end: n8n execution 86, a proof-verification job graded with RUBRIC_v1.
145
+
146
+ - Answers recorded in devnet tx
147
+ [`0x9a38cf55…a61b`](https://www.taifoon.io/scan/36927/tx/0x9a38cf5541bb82a8a6e2e5cf1a96ed25fdc46921c351506da83023f5dec1a61b).
148
+ The answers digest is `0x6c7e6d2977689f1c9639a377b165f998d1160cc30eca8ff4f1fedc793db2f52d`.
149
+ - Decision anchored in devnet tx
150
+ [`0x687ba9d7…4ee4`](https://www.taifoon.io/scan/36927/tx/0x687ba9d73d5789a2df9d9d09e94b6ed005447d7f5ece97f9226ea0d51f514ee4).
151
+ Decision digest `0x781b0fce…3667`, confidence 2,700 bps.
152
+ - Jev answered spec_met `{ yes: 0.36, no: 0.64 }`, so the composed verdict is **reject** (spec_met ≤ 0.40).
153
+
154
+ `verify('0x6c7e6d29…f52d')` finds both rows. This is the real output:
155
+
156
+ ```
157
+ {"ok":true,"checks":{"answersOnChain":true,"decisionOnChain":true},
158
+ "answer":{"tx":"0x9a38cf5541bb82a8a6e2e5cf1a96ed25fdc46921c351506da83023f5dec1a61b","trusted":true},
159
+ "decision":{"tx":"0x687ba9d73d5789a2df9d9d09e94b6ed005447d7f5ece97f9226ea0d51f514ee4","confidenceBps":2700}}
160
+ ```
161
+
162
+ The tests re-derive these rows offline, byte for byte:
163
+
164
+ - **Execution 86:** the answers digest, the `JevAnswerLog.record` calldata (equal to the mined input) and the decision
165
+ digest.
166
+ - **Execution 88:** run through `grade()` itself, it lands on the anchored decision digest and composes needs_review.
167
+
168
+ ## The records
169
+
170
+ - **Receipt.** `receiptHash = sha256(JSON.stringify(body))`. The body holds the rubric and its hash, the thresholds, the
171
+ subject, `stateHash`, the facts, the model, every answer with its distribution, what was not asked, the verdict,
172
+ `auto`, `forced`, the reasons and the scores.
173
+ - **decision.v2.** `digest = sha256(canonical { v, kind, subject{chainId, at, ref}, model, answers[id, question,
174
+ options, value, probabilities, confidence], input_digest })`, with keys sorted.
175
+ `subjectId = keccak256(abi.encode("taifoon.decision.subject.v1", chainId, at, ref))`.
176
+ - **jev.answer.v1.** `digest = sha256(canonical record)`, covering questions, answers with distributions, model,
177
+ upstream model, latency, credential path (`trial` or `caller-credential`), caller and time.
178
+
179
+ ## In n8n
180
+
181
+ The TypeSafe node (`@taifoon/n8n-nodes-typesafe` 1.5.0) carries this package's network-free core under **Jev Options**:
182
+
183
+ - **Ask RUBRIC_v1** routes Pass = complete, Fail = reject and Review = needs_review.
184
+ - **Facts (JSON)** takes checks you already made. A false check rejects without asking Jev.
185
+ - **Record On** takes `none`, `devnet`, `base` or `both`, and **Evaluator Call** takes a protocol and a job id.
186
+
187
+ The node outputs the receipt and the unsigned calls, and it signs nothing. With the options unset it behaves exactly as
188
+ 1.4.0.
189
+
190
+ ## Workflows
191
+
192
+ `workflows/0.1.0/` holds the four n8n workflows that run Jev on n8n.taifoon.dev. The same files are attached to the
193
+ GitHub release as `jev-workflows-0.1.0.zip`:
194
+
195
+ - **`hire-judge-settle.json`** runs hire → prepare (facts) → TypeSafe (the four RUBRIC_v1 questions) → answers
196
+ (verdict, recorded and anchored) → settle on devnet. Executions 86 and 88 of this workflow are the golden runs above.
197
+ - **`jev-grader.json`** grades Base jobs nobody ruled on, then stamps each verdict on the devnet GradeStampRegistry.
198
+ - **`batch-judge.json`** runs prepare → Jev → answers for each subject, then `POST /v1/judge/batch`.
199
+ - **`stamp-grade.json`** takes a recorded decision, turns it into the canonical verdict and stamps it.
200
+
201
+ Credential references are placeholders: `{ "id": "", "name": "REPLACE: <credential type>" }`. After import, pick your
202
+ own credential of that type. Each step names the entity it takes and emits by `$id` (`meta.taifoon.steps`). The schemas
203
+ behind those `$id`s are in `workflows/0.1.0/schemas/`. `manifest.json` pins every file by sha256.
204
+ `workflows/0.1.0/README.md` explains each workflow step by step and lists the nodes and credentials it needs:
205
+
206
+ - `@taifoon/n8n-nodes-typesafe`, tested with 1.3.0, is on npm.
207
+ - `n8n-nodes-taifoon`, tested with 0.4.2, and `n8n-nodes-taifoon-devnet-signer`, tested with 0.2.0, are custom
208
+ extensions and are not on npm.
209
+
210
+ ```
211
+ npx @taifoon/jev workflows list
212
+ npx @taifoon/jev workflows export ./jev-workflows # workflows + schemas + manifest + README
213
+ npx @taifoon/jev verify 0x6c7e6d2977689f1c9639a377b165f998d1160cc30eca8ff4f1fedc793db2f52d
214
+ ```
215
+
216
+ ## Develop
217
+
218
+ ```
219
+ npm ci && npm run check # typecheck, tests (golden runs, recorded calldata, hashes vs viem), build
220
+ node scripts/vendor-n8n.mjs # refresh the n8n node's copy of the core; test/vendor.test.ts fails when it drifts
221
+ npm run bundle && npm run zip # rebuild workflows/<version>/ from the umbrella's workflows/ and zip it for the release
222
+ ```
package/bin/jev.mjs ADDED
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ // jev — the @taifoon/jev command line.
3
+ // jev workflows list [--version X] the bundled n8n workflows (and which nodes and credentials each needs)
4
+ // jev workflows export <dir> [--version X] write them, their schemas, manifest and README into <dir>
5
+ // jev verify <answers-digest> find the digest's JevAnswered / Decided rows on devnet 36927
6
+ import { cpSync, existsSync, readdirSync, readFileSync } from 'node:fs';
7
+ import { dirname, join, resolve } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
11
+ const PKG = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8'));
12
+ const args = process.argv.slice(2);
13
+ const flag = (n) => { const i = args.indexOf(n); return i >= 0 ? args.splice(i, 2)[1] : undefined; };
14
+ const version = flag('--version') ?? PKG.version;
15
+ const bundle = join(ROOT, 'workflows', version);
16
+ const die = (m) => { console.error(m); process.exit(1); };
17
+ const [cmd, sub, target] = args;
18
+
19
+ if (cmd === 'workflows') {
20
+ if (!existsSync(bundle)) die(`no workflow bundle ${version}; bundled: ${readdirSync(join(ROOT, 'workflows')).join(', ')}`);
21
+ const m = JSON.parse(readFileSync(join(bundle, 'manifest.json'), 'utf8'));
22
+ if (sub === 'list') {
23
+ for (const w of m.workflows) console.log(`${w.file} ${w.name}\n nodes: ${w.packages.join(', ')} · credentials: ${w.credentials.join(', ') || 'none'}`);
24
+ } else if (sub === 'export') {
25
+ if (!target) die('usage: jev workflows export <dir> [--version X]');
26
+ const out = resolve(target);
27
+ cpSync(bundle, out, { recursive: true });
28
+ console.log(`${m.bundle} → ${out}: ${m.workflows.length} workflows, ${m.schemas.length} schemas, manifest.json, README.md`);
29
+ } else die('usage: jev workflows list|export <dir> [--version X]');
30
+ } else if (cmd === 'verify' && sub) {
31
+ const { verify } = await import('../dist/index.js');
32
+ const v = await verify(sub);
33
+ console.log(JSON.stringify(v, null, 1));
34
+ process.exit(v.ok ? 0 : 2);
35
+ } else {
36
+ console.log(`jev ${PKG.version}\n jev workflows list [--version X]\n jev workflows export <dir> [--version X]\n jev verify <answers-digest>`);
37
+ }
package/dist/abi.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { type Hex } from './hash.js';
2
+ export type AbiType = 'bytes32' | 'uint256' | 'uint64' | 'uint16' | 'uint8' | 'bool' | 'address' | 'bytes' | 'string';
3
+ export type AbiValue = string | number | bigint | boolean;
4
+ /** abi.encode(values) for the given types */
5
+ export declare function encode(types: readonly AbiType[], values: readonly AbiValue[]): Hex;
6
+ /** the 4-byte selector of a canonical signature, e.g. "complete(bytes32)" */
7
+ export declare const selector: (signature: string) => Hex;
8
+ /** calldata: selector ‖ abi.encode(args); the types are read from the signature */
9
+ export declare function encodeCall(signature: string, values: readonly AbiValue[]): Hex;
10
+ /** abi.decode for the same types (log data) */
11
+ export declare function decode(types: readonly AbiType[], data: Hex): AbiValue[];
package/dist/abi.js ADDED
@@ -0,0 +1,81 @@
1
+ // The ABI encoding this package needs, and no more: static words (bytes32, uintN, bool, address) and dynamic bytes /
2
+ // string, for calls and for reading the two log events. Checked against viem in test/abi.test.ts.
3
+ import { fromHex, keccakHex, toHex, utf8 } from './hash.js';
4
+ const word = (n) => { if (n < 0n || n >= 1n << 256n)
5
+ throw new Error('uint out of range'); return n.toString(16).padStart(64, '0'); };
6
+ const isDynamic = (t) => t === 'bytes' || t === 'string';
7
+ function staticWord(t, v) {
8
+ switch (t) {
9
+ case 'bool': return word(v ? 1n : 0n);
10
+ case 'address': {
11
+ const s = String(v);
12
+ if (!/^0x[0-9a-fA-F]{40}$/.test(s))
13
+ throw new Error(`not an address: ${s}`);
14
+ return s.slice(2).toLowerCase().padStart(64, '0');
15
+ }
16
+ case 'bytes32': {
17
+ const s = String(v);
18
+ if (!/^0x[0-9a-fA-F]{64}$/.test(s))
19
+ throw new Error(`not a bytes32: ${s}`);
20
+ return s.slice(2).toLowerCase();
21
+ }
22
+ default: {
23
+ const bits = Number(t.slice(4));
24
+ const n = BigInt(v);
25
+ if (n < 0n || n >= 1n << BigInt(bits))
26
+ throw new Error(`${String(v)} does not fit ${t}`);
27
+ return word(n);
28
+ }
29
+ }
30
+ }
31
+ function tail(t, v) {
32
+ const b = t === 'string' ? utf8(String(v)) : fromHex(String(v));
33
+ const hex = toHex(b).slice(2);
34
+ return word(BigInt(b.length)) + hex.padEnd(Math.ceil(hex.length / 64) * 64, '0');
35
+ }
36
+ /** abi.encode(values) for the given types */
37
+ export function encode(types, values) {
38
+ if (types.length !== values.length)
39
+ throw new Error('types and values differ in length');
40
+ let head = '';
41
+ let body = '';
42
+ const headLen = types.length * 32;
43
+ types.forEach((t, i) => {
44
+ if (isDynamic(t)) {
45
+ head += word(BigInt(headLen + body.length / 2));
46
+ body += tail(t, values[i]);
47
+ }
48
+ else
49
+ head += staticWord(t, values[i]);
50
+ });
51
+ return `0x${head}${body}`;
52
+ }
53
+ /** the 4-byte selector of a canonical signature, e.g. "complete(bytes32)" */
54
+ export const selector = (signature) => keccakHex(signature).slice(0, 10);
55
+ /** calldata: selector ‖ abi.encode(args); the types are read from the signature */
56
+ export function encodeCall(signature, values) {
57
+ const inner = signature.slice(signature.indexOf('(') + 1, -1);
58
+ const types = (inner ? inner.split(',') : []);
59
+ return `${selector(signature)}${encode(types, values).slice(2)}`;
60
+ }
61
+ /** abi.decode for the same types (log data) */
62
+ export function decode(types, data) {
63
+ const h = data.slice(2);
64
+ const at = (i) => h.slice(i * 64, i * 64 + 64);
65
+ return types.map((t, i) => {
66
+ const w = at(i);
67
+ if (isDynamic(t)) {
68
+ const off = Number(BigInt('0x' + w)) * 2;
69
+ const len = Number(BigInt('0x' + h.slice(off, off + 64)));
70
+ const raw = h.slice(off + 64, off + 64 + len * 2);
71
+ return t === 'string' ? new TextDecoder().decode(fromHex(raw)) : `0x${raw}`;
72
+ }
73
+ if (t === 'bool')
74
+ return BigInt('0x' + w) !== 0n;
75
+ if (t === 'address')
76
+ return `0x${w.slice(24)}`;
77
+ if (t === 'bytes32')
78
+ return `0x${w}`;
79
+ return BigInt('0x' + w);
80
+ });
81
+ }
package/dist/ask.d.ts ADDED
@@ -0,0 +1,27 @@
1
+ import type { Answer, Question } from './rubric.js';
2
+ export declare const TRIAL_URL = "https://typesafe.taifoon.dev/v1/trial";
3
+ export declare const TYPESAFE_URL = "https://api.typesafe.ai";
4
+ export declare const TRIAL_MAX_STATE = 4000;
5
+ export type Asked = {
6
+ answers: Record<string, Answer>;
7
+ model: string | null;
8
+ latency_ms: number | null;
9
+ connection: 'trial' | 'key';
10
+ trial: {
11
+ calls: number;
12
+ left: number;
13
+ } | null;
14
+ };
15
+ export declare class JevError extends Error {
16
+ readonly status: number;
17
+ readonly next?: string | undefined;
18
+ constructor(message: string, status: number, next?: string | undefined);
19
+ }
20
+ export declare function askJev(a: {
21
+ text: string;
22
+ questions: readonly Question[];
23
+ key?: string | null;
24
+ trialUrl?: string;
25
+ endpoint?: string;
26
+ fetch?: typeof fetch;
27
+ }): Promise<Asked>;
package/dist/ask.js ADDED
@@ -0,0 +1,47 @@
1
+ export const TRIAL_URL = 'https://typesafe.taifoon.dev/v1/trial';
2
+ export const TYPESAFE_URL = 'https://api.typesafe.ai';
3
+ export const TRIAL_MAX_STATE = 4_000;
4
+ export class JevError extends Error {
5
+ status;
6
+ next;
7
+ constructor(message, status, next) {
8
+ super(message);
9
+ this.status = status;
10
+ this.next = next;
11
+ this.name = 'JevError';
12
+ }
13
+ }
14
+ const norm = (id, a) => ({ id, value: String(a.value ?? a.choice ?? ''), confidence: Number(a.confidence ?? 0), probabilities: a.probabilities ?? {} });
15
+ export async function askJev(a) {
16
+ const f = a.fetch ?? fetch;
17
+ const started = Date.now();
18
+ if (a.key) {
19
+ const base = (a.endpoint ?? TYPESAFE_URL).replace(/\/$/, '');
20
+ if (!/^https:\/\//.test(base))
21
+ throw new JevError('a key is only sent over https', 400);
22
+ const questions = Object.fromEntries(a.questions.map((q) => [q.id, { type: 'choice', instructions: q.text, criteria: Object.fromEntries(q.options.map((o) => [o, o])) }]));
23
+ const r = await f(`${base}/v1/systemone`, { method: 'POST', headers: { 'content-type': 'application/json', authorization: `Bearer ${a.key}` }, body: JSON.stringify({ model: 'jev-latest', state: a.text, questions }), signal: AbortSignal.timeout(65_000) });
24
+ const j = (await r.json().catch(() => ({})));
25
+ if (!r.ok)
26
+ throw new JevError(`TypeSafe answered ${r.status}${r.status === 401 || r.status === 403 ? ': the key was rejected' : r.status === 402 ? ': the account has no credit' : ''}`, r.status);
27
+ const answers = {};
28
+ for (const q of a.questions) {
29
+ const x = j.answers?.[q.id];
30
+ if (x)
31
+ answers[q.id] = norm(q.id, x);
32
+ }
33
+ return { answers, model: j.model ?? null, latency_ms: Date.now() - started, connection: 'key', trial: null };
34
+ }
35
+ const body = { state: a.text, questions: a.questions.map((q) => ({ id: q.id, kind: 'choice', text: q.text, options: [...q.options] })) };
36
+ const r = await f(a.trialUrl ?? TRIAL_URL, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body), signal: AbortSignal.timeout(65_000) });
37
+ const j = (await r.json().catch(() => ({})));
38
+ if (!r.ok || j.ok === false)
39
+ throw new JevError(`the free trial answered ${r.status}: ${j.error ?? 'refused'}`, r.status, j.next ?? 'use your own TypeSafe key: grade({ key })');
40
+ const answers = {};
41
+ for (const x of j.answers ?? []) {
42
+ const id = String(x.id ?? '');
43
+ if (id && x.schema_ok !== false)
44
+ answers[id] = norm(id, x);
45
+ }
46
+ return { answers, model: j.upstreamModel ?? j.model ?? null, latency_ms: typeof j.latency_ms === 'number' ? j.latency_ms : Date.now() - started, connection: 'trial', trial: j.trial ? { calls: j.trial.calls, left: j.trial.left } : null };
47
+ }
@@ -0,0 +1,37 @@
1
+ export declare const CONTRACTS: Readonly<{
2
+ /** the public devnet the logs live on: chain 36927, free gas; anyone may send */
3
+ readonly devnet: {
4
+ readonly chainId: 36927;
5
+ readonly rpc: "https://rpc.taifoon.dev";
6
+ /** JevAnswerLog: append-only, no owner. Recorders fixed at deploy are `trusted`; any other sender is logged with trusted = false */
7
+ readonly answerLog: {
8
+ readonly address: "0xD8c1d8188Fc8EE388792f6EB3B3dbd4f341Ba8e3";
9
+ readonly fromBlock: 1106754;
10
+ };
11
+ /** JevDecisionLog: append-only, no owner, no upgrade; the recorder is msg.sender */
12
+ readonly decisionLog: {
13
+ readonly address: "0x1D622511862DD7DEffB8fEeBc225E0FAdA1e9A05";
14
+ readonly fromBlock: 981099;
15
+ };
16
+ /** JudgeAdapter: posts a verdict (with the decision digest) to the devnet assurance hook */
17
+ readonly judgeAdapter: "0xbd4c9e797a8DBbfdde9C77bc5A5b21ACf611Db8f";
18
+ readonly assuranceHook: "0x7110c8951d17b69054395742e265bbd98259564a";
19
+ };
20
+ /** Base (8453). answerLog / decisionLog: null in this version → record({ network: 'base' }) returns the call data with `to: null` */
21
+ readonly base: {
22
+ readonly chainId: 8453;
23
+ readonly rpc: "https://mainnet.base.org";
24
+ readonly answerLog: {
25
+ address: string;
26
+ fromBlock: number;
27
+ } | null;
28
+ readonly decisionLog: {
29
+ address: string;
30
+ fromBlock: number;
31
+ } | null;
32
+ readonly virtualsErc8183: "0x238E541BfefD82238730D00a2208E5497F1832E0";
33
+ readonly virtualsMemoAcpRouter: "0xa6C9BA866992cfD7fd6460ba912bfa405adA9df0";
34
+ readonly bitagentErc8183: "0x5009ABB3A309115a4a682C66BAf3BC9E0329BaB7";
35
+ readonly bitagentEvaluator: "0x4302e523D982f3b89Cfc43cE4530C012b495Ec11";
36
+ };
37
+ }>;
@@ -0,0 +1,27 @@
1
+ // Where the records and the evaluator seats live. Every address here was read from the chain or from its deploy
2
+ // broadcast; nothing is deployed by this package.
3
+ export const CONTRACTS = Object.freeze({
4
+ /** the public devnet the logs live on: chain 36927, free gas; anyone may send */
5
+ devnet: {
6
+ chainId: 36927,
7
+ rpc: 'https://rpc.taifoon.dev',
8
+ /** JevAnswerLog: append-only, no owner. Recorders fixed at deploy are `trusted`; any other sender is logged with trusted = false */
9
+ answerLog: { address: '0xD8c1d8188Fc8EE388792f6EB3B3dbd4f341Ba8e3', fromBlock: 1_106_754 },
10
+ /** JevDecisionLog: append-only, no owner, no upgrade; the recorder is msg.sender */
11
+ decisionLog: { address: '0x1D622511862DD7DEffB8fEeBc225E0FAdA1e9A05', fromBlock: 981_099 },
12
+ /** JudgeAdapter: posts a verdict (with the decision digest) to the devnet assurance hook */
13
+ judgeAdapter: '0xbd4c9e797a8DBbfdde9C77bc5A5b21ACf611Db8f',
14
+ assuranceHook: '0x7110c8951d17b69054395742e265bbd98259564a',
15
+ },
16
+ /** Base (8453). answerLog / decisionLog: null in this version → record({ network: 'base' }) returns the call data with `to: null` */
17
+ base: {
18
+ chainId: 8453,
19
+ rpc: 'https://mainnet.base.org',
20
+ answerLog: null,
21
+ decisionLog: null,
22
+ virtualsErc8183: '0x238E541BfefD82238730D00a2208E5497F1832E0',
23
+ virtualsMemoAcpRouter: '0xa6C9BA866992cfD7fd6460ba912bfa405adA9df0',
24
+ bitagentErc8183: '0x5009ABB3A309115a4a682C66BAf3BC9E0329BaB7',
25
+ bitagentEvaluator: '0x4302e523D982f3b89Cfc43cE4530C012b495Ec11',
26
+ },
27
+ });
@@ -0,0 +1,6 @@
1
+ import { type Adapter } from './types.js';
2
+ export declare const ABI: {
3
+ readonly complete: "complete(bytes32)";
4
+ readonly reject: "reject(bytes32,bytes32)";
5
+ };
6
+ export declare const assuranceHook: Adapter;
@@ -0,0 +1,15 @@
1
+ // An assurance hook (the AssuranceHook family: Taifoon's, Moonbeam's on Base). The job's evaluator — or its buyer —
2
+ // ends it: complete(jobId) pays the seller; reject(jobId, digest) walks every party back and records the digest.
3
+ import { encodeCall } from '../abi.js';
4
+ import { asBytes32, need } from './types.js';
5
+ export const ABI = { complete: 'complete(bytes32)', reject: 'reject(bytes32,bytes32)' };
6
+ export const assuranceHook = {
7
+ name: 'assurance-hook',
8
+ about: 'AssuranceHook: complete(jobId) or reject(jobId, digest); pass the hook address as opts.to',
9
+ call: (jobId, verdict, digest, o) => ({
10
+ protocol: 'assurance-hook', chainId: o.chainId ?? 8453, to: need(o.to, 'the hook address'), value: '0',
11
+ fn: verdict === 'complete' ? ABI.complete : ABI.reject,
12
+ data: verdict === 'complete' ? encodeCall(ABI.complete, [asBytes32(jobId)]) : encodeCall(ABI.reject, [asBytes32(jobId), digest]),
13
+ signer: 'the job’s buyer or an evaluator the hook registered',
14
+ }),
15
+ };
@@ -0,0 +1,7 @@
1
+ import { type Adapter } from './types.js';
2
+ export declare const ABI: {
3
+ readonly complete: "complete(uint256,bytes32,bytes)";
4
+ readonly reject: "reject(uint256,bytes32,bytes)";
5
+ readonly forceSettleJob: "forceSettleJob(uint256,bool)";
6
+ };
7
+ export declare const bitagentErc8183: Adapter;
@@ -0,0 +1,18 @@
1
+ // BitAgent / Unibase ERC-8183 (AgenticCommerce on Base; implementation unverified, layout read from its transactions).
2
+ // seat "job" (default): you are the job's evaluator → AgenticCommerce complete / reject(jobId, digest, 0x), the same
3
+ // (uint256, bytes32, bytes) layout its submit uses.
4
+ // seat "platform": the platform Evaluator contract's forceSettleJob(jobId, approved) — what settles its jobs today;
5
+ // only BitAgent's operators may send it.
6
+ import { encodeCall } from '../abi.js';
7
+ import { CONTRACTS } from '../contracts.js';
8
+ import { asUint } from './types.js';
9
+ export const ABI = { complete: 'complete(uint256,bytes32,bytes)', reject: 'reject(uint256,bytes32,bytes)', forceSettleJob: 'forceSettleJob(uint256,bool)' };
10
+ export const bitagentErc8183 = {
11
+ name: 'bitagent-erc8183',
12
+ about: 'BitAgent AgenticCommerce: complete / reject(jobId, digest, 0x) as evaluator; seat "platform" = Evaluator.forceSettleJob',
13
+ call: (jobId, verdict, digest, o) => o.seat === 'platform'
14
+ ? { protocol: 'bitagent-erc8183', chainId: o.chainId ?? CONTRACTS.base.chainId, to: o.to ?? CONTRACTS.base.bitagentEvaluator, value: '0', fn: ABI.forceSettleJob,
15
+ data: encodeCall(ABI.forceSettleJob, [asUint(jobId), verdict === 'complete']), signer: 'a BitAgent platform operator (the Evaluator contract’s own access control)' }
16
+ : { protocol: 'bitagent-erc8183', chainId: o.chainId ?? CONTRACTS.base.chainId, to: o.to ?? CONTRACTS.base.bitagentErc8183, value: '0', fn: ABI[verdict],
17
+ data: encodeCall(ABI[verdict], [asUint(jobId), digest, '0x']), signer: 'the address the job names as evaluator' },
18
+ };
@@ -0,0 +1,12 @@
1
+ import type { Verdict } from '../rubric.js';
2
+ import type { Adapter, AdapterOpts, EvaluatorCall } from './types.js';
3
+ export declare const ADAPTERS: Readonly<Record<string, Adapter>>;
4
+ export type Protocol = 'assurance-hook' | 'judge-adapter' | 'virtuals-erc8183' | 'virtuals-memo-acp' | 'bitagent-erc8183';
5
+ export declare function evaluatorCall(protocol: Protocol | Adapter, jobId: string | number | bigint, verdict: Verdict | {
6
+ verdict: Verdict;
7
+ }, digest: string | {
8
+ decision: {
9
+ digest: string;
10
+ } | null;
11
+ }, opts?: AdapterOpts): EvaluatorCall | null;
12
+ export type { Adapter, AdapterOpts, EvaluatorCall };
@@ -0,0 +1,20 @@
1
+ import { assuranceHook } from './assurance-hook.js';
2
+ import { bitagentErc8183 } from './bitagent-erc8183.js';
3
+ import { judgeAdapter } from './judge-adapter.js';
4
+ import { virtualsErc8183 } from './virtuals-erc8183.js';
5
+ import { virtualsMemoAcp } from './virtuals-memo-acp.js';
6
+ export const ADAPTERS = Object.freeze(Object.fromEntries([assuranceHook, judgeAdapter, virtualsErc8183, virtualsMemoAcp, bitagentErc8183].map((a) => [a.name, a])));
7
+ export function evaluatorCall(protocol, jobId, verdict, digest, opts = {}) {
8
+ const a = typeof protocol === 'string' ? ADAPTERS[protocol] : protocol;
9
+ if (!a)
10
+ throw new Error(`unknown protocol ${String(protocol)}; one of ${Object.keys(ADAPTERS).join(', ')}`);
11
+ const v = typeof verdict === 'string' ? verdict : verdict.verdict;
12
+ if (v === 'needs_review')
13
+ return null;
14
+ if (v !== 'complete' && v !== 'reject')
15
+ throw new Error(`verdict is complete | reject | needs_review, got ${String(v)}`);
16
+ const d = typeof digest === 'string' ? digest : digest.decision?.digest;
17
+ if (!d || !/^0x[0-9a-fA-F]{64}$/.test(d))
18
+ throw new Error('digest: the decision digest (receipt.decision.digest), 32 bytes');
19
+ return a.call(jobId, v, d.toLowerCase(), opts);
20
+ }
@@ -0,0 +1,5 @@
1
+ import { type Adapter } from './types.js';
2
+ export declare const ABI: {
3
+ readonly postVerdict: "postVerdict(bytes32,uint8,uint8,bytes32)";
4
+ };
5
+ export declare const judgeAdapter: Adapter;
@@ -0,0 +1,16 @@
1
+ // JudgeAdapter: the evaluator seat between a judge and an assurance hook. postVerdict(jobId, 1 Conformant | 2
2
+ // NonConformant, mode 2 calibrated, digest) stores the verdict once per job and calls hook.complete / hook.reject in the
3
+ // same transaction, so the adapter's VerdictPosted, the hook's event and the decision record carry one digest.
4
+ import { encodeCall } from '../abi.js';
5
+ import { CONTRACTS } from '../contracts.js';
6
+ import { asBytes32 } from './types.js';
7
+ export const ABI = { postVerdict: 'postVerdict(bytes32,uint8,uint8,bytes32)' };
8
+ export const judgeAdapter = {
9
+ name: 'judge-adapter',
10
+ about: 'JudgeAdapter.postVerdict(jobId, 1|2, 2, digest) → hook.complete / hook.reject (devnet by default)',
11
+ call: (jobId, verdict, digest, o) => ({
12
+ protocol: 'judge-adapter', chainId: o.chainId ?? CONTRACTS.devnet.chainId, to: o.to ?? CONTRACTS.devnet.judgeAdapter, value: '0', fn: ABI.postVerdict,
13
+ data: encodeCall(ABI.postVerdict, [asBytes32(jobId), verdict === 'complete' ? 1 : 2, 2, digest]),
14
+ signer: 'an evaluator registered on the adapter (isEvaluator)',
15
+ }),
16
+ };