@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.
- package/LICENSE +18 -0
- package/README.md +222 -0
- package/bin/jev.mjs +37 -0
- package/dist/abi.d.ts +11 -0
- package/dist/abi.js +81 -0
- package/dist/ask.d.ts +27 -0
- package/dist/ask.js +47 -0
- package/dist/contracts.d.ts +37 -0
- package/dist/contracts.js +27 -0
- package/dist/evaluator/assurance-hook.d.ts +6 -0
- package/dist/evaluator/assurance-hook.js +15 -0
- package/dist/evaluator/bitagent-erc8183.d.ts +7 -0
- package/dist/evaluator/bitagent-erc8183.js +18 -0
- package/dist/evaluator/index.d.ts +12 -0
- package/dist/evaluator/index.js +20 -0
- package/dist/evaluator/judge-adapter.d.ts +5 -0
- package/dist/evaluator/judge-adapter.js +16 -0
- package/dist/evaluator/types.d.ts +27 -0
- package/dist/evaluator/types.js +6 -0
- package/dist/evaluator/virtuals-erc8183.d.ts +6 -0
- package/dist/evaluator/virtuals-erc8183.js +15 -0
- package/dist/evaluator/virtuals-memo-acp.d.ts +5 -0
- package/dist/evaluator/virtuals-memo-acp.js +15 -0
- package/dist/facts.d.ts +16 -0
- package/dist/facts.js +15 -0
- package/dist/grade.d.ts +32 -0
- package/dist/grade.js +33 -0
- package/dist/hash.d.ts +10 -0
- package/dist/hash.js +132 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +20 -0
- package/dist/receipt.d.ts +68 -0
- package/dist/receipt.js +42 -0
- package/dist/record.d.ts +32 -0
- package/dist/record.js +41 -0
- package/dist/records.d.ts +78 -0
- package/dist/records.js +55 -0
- package/dist/rubric.d.ts +109 -0
- package/dist/rubric.js +91 -0
- package/dist/verify.d.ts +40 -0
- package/dist/verify.js +80 -0
- package/package.json +73 -0
- package/workflows/0.1.0/README.md +104 -0
- package/workflows/0.1.0/batch-judge.json +341 -0
- package/workflows/0.1.0/hire-judge-settle.json +767 -0
- package/workflows/0.1.0/jev-grader.json +433 -0
- package/workflows/0.1.0/manifest.json +151 -0
- package/workflows/0.1.0/schemas/answers.json +248 -0
- package/workflows/0.1.0/schemas/common.json +394 -0
- package/workflows/0.1.0/schemas/delivery.json +233 -0
- package/workflows/0.1.0/schemas/facts.json +225 -0
- package/workflows/0.1.0/schemas/hire.json +337 -0
- package/workflows/0.1.0/schemas/job.json +96 -0
- package/workflows/0.1.0/schemas/settlement.json +225 -0
- package/workflows/0.1.0/schemas/verdict.json +314 -0
- 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,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,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
|
+
};
|