@taifoon/jev 0.2.17 → 0.3.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/README.md +16 -10
- package/bin/run.mjs +7 -6
- package/dist/ask.d.ts +18 -2
- package/dist/ask.js +30 -0
- package/dist/attest.d.ts +129 -0
- package/dist/attest.js +223 -0
- package/dist/grade.d.ts +5 -0
- package/dist/grade.js +6 -3
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -1
- package/dist/pipeline.d.ts +5 -3
- package/dist/pipeline.js +10 -5
- package/dist/record.d.ts +17 -4
- package/dist/record.js +35 -27
- package/dist/rpc-fallback.js +1 -1
- package/docs/API.md +1 -1
- package/docs/PIPELINE.md +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -14,8 +14,9 @@ npx @taifoon/jev run --demo
|
|
|
14
14
|
|
|
15
15
|

|
|
16
16
|
|
|
17
|
-
Then grade your own with your
|
|
18
|
-
|
|
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. Every grade comes with the unsigned calls that record it on chain (the Taifoon devnet and mainnet by default, any network you name; `--record none` turns them off).
|
|
19
20
|
|
|
20
21
|
## Quick start
|
|
21
22
|
|
|
@@ -55,7 +56,7 @@ claims the evidence cannot support. Grade one with your key:
|
|
|
55
56
|
```
|
|
56
57
|
TYPESAFE_KEY=… npx @taifoon/jev run --job-file examples/jobs/invoice-extraction.json
|
|
57
58
|
npx @taifoon/jev run --demo # offline, no key: replays the Base job below and checks it against the chain
|
|
58
|
-
npx @taifoon/jev run # a live Base job from the coordination layer's queue (
|
|
59
|
+
npx @taifoon/jev run # a live Base job from the coordination layer's queue (your TYPESAFE_KEY, else the free grades)
|
|
59
60
|
```
|
|
60
61
|
|
|
61
62
|
## A Base job, graded
|
|
@@ -105,7 +106,7 @@ the output says how to retry. Set `record` to `none` to grade without writing on
|
|
|
105
106
|
|
|
106
107
|
## `jev run` flags
|
|
107
108
|
|
|
108
|
-
Safe by default:
|
|
109
|
+
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.
|
|
109
110
|
|
|
110
111
|
| Flag | Default | What it does |
|
|
111
112
|
|---|---|---|
|
|
@@ -114,15 +115,16 @@ Safe by default: your own TypeSafe key only, nothing recorded, nothing signed or
|
|
|
114
115
|
| `--job-file job.json` | | Grade a job you describe (see `examples/jobs/`) |
|
|
115
116
|
| `--evidence pack.json` | | Grade your own evidence pack, no layer |
|
|
116
117
|
| `--answers answers.json` | | Answers you already have (e.g. from the n8n node); Jev is not asked |
|
|
117
|
-
| `--record none\|devnet\|base\|both` |
|
|
118
|
+
| `--record none\|devnet\|base\|both` | Taifoon devnet + mainnet | Where the unsigned calls that record and stamp the grade point; `none` turns them off |
|
|
118
119
|
| `--layer <https url>` / `--no-layer` | `https://coord.taifoon.dev` | Where jobs and quotes are read |
|
|
119
120
|
| `--protocol <name>` | from the job id | Evaluator seat: `virtuals-erc8183`, `virtuals-memo-acp`, `bitagent-erc8183`, `assurance-hook`, `judge-adapter` |
|
|
120
121
|
| `--price-usdc <n>` | the job's budget | Price for the premium quote |
|
|
121
122
|
| `--seller-record <k>/<n>` | read from the layer | Price the premium here from the seller's record (k delivered of n graded) with [`@taifoon/jev-wilson`](https://www.npmjs.com/package/@taifoon/jev-wilson): the layer's numbers, no request |
|
|
122
123
|
| `--yes` / `--json` | ask per step / text | Run every step without asking / print the whole trace as JSON |
|
|
123
124
|
|
|
124
|
-
Environment: `TYPESAFE_KEY` (your key; without it a job that needs Jev
|
|
125
|
-
|
|
125
|
+
Environment: `TYPESAFE_KEY` (your key; without it a job that needs Jev is graded on your free grades on the layer, and
|
|
126
|
+
past them it stops and says how to pay one grade; a job the code checks reject is graded either way; `--no-layer`, `--demo` and `--job-file` ask
|
|
127
|
+
no one). `TAIFOON_RELAYER_KEY` pays layer grades past the free ones from that key's balance. Exit codes: `0` done, `1` a step failed, `2` a flag was refused.
|
|
126
128
|
|
|
127
129
|
## What you must know
|
|
128
130
|
|
|
@@ -132,8 +134,12 @@ code checks reject is still graded). Exit codes: `0` done, `1` a step failed, `2
|
|
|
132
134
|
- **Code composes the verdict** under THRESHOLDS_v1: complete, reject or needs_review. needs_review ends nothing and
|
|
133
135
|
leaves room for an appeal. Above 50 USDC a complete is held for review.
|
|
134
136
|
- **Nothing is signed.** `record()` and `evaluatorCall()` return unsigned calls. Whoever holds the seat signs.
|
|
135
|
-
- **Your key
|
|
136
|
-
directly at TypeSafe
|
|
137
|
+
- **Your key, or the free grades.** With your own TypeSafe key from [console.typesafe.ai](https://console.typesafe.ai),
|
|
138
|
+
Jev is called directly at TypeSafe; the key is not stored and never appears in a receipt. Without one, `grade()`
|
|
139
|
+
claims one of your free grades on the coordination layer (`POST https://coord.taifoon.dev/v1/judge/compose`, 3 a day
|
|
140
|
+
per caller): the layer asks Jev the same four questions and this package composes the verdict from the answers, so the
|
|
141
|
+
receipt is the same shape (`via.connection: "trial"`, with the grades left). Past them the layer answers 402 with the
|
|
142
|
+
price of one grade (x402), or pass `layerKey` (a Taifoon key) to pay from its balance. `layer: false` turns this off.
|
|
137
143
|
- **What the result is.** *Graded against a published rubric by a pinned decision model, with an appeal.* It is not
|
|
138
144
|
"independently verified". An on-chain record is an attestation by whoever sent it, not a proof the answer is right.
|
|
139
145
|
|
|
@@ -211,4 +217,4 @@ In code: `verify(digest, { network: 'arbitrum' })` (a name or a chain id), `{ rp
|
|
|
211
217
|
|
|
212
218
|
Independent project. Jev and TypeSafe are products of TypeSafe AI, Inc., which does not endorse this package.
|
|
213
219
|
|
|
214
|
-
Built by Taifoon. MIT. Its runtime dependencies are Taifoon's own packages, @taifoon/jev-wilson (the pricing) and @taifoon/n8n-nodes-typesafe; nothing third-party. It contains no key. It calls Jev
|
|
220
|
+
Built by Taifoon. MIT. Its runtime dependencies are Taifoon's own packages, @taifoon/jev-wilson (the pricing) and @taifoon/n8n-nodes-typesafe; nothing third-party. It contains no key. It calls Jev with your own TypeSafe key, or without one through your free grades on the Taifoon coordination layer.
|
package/bin/run.mjs
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
// jev run — one job through the pipeline, one step at a time.
|
|
2
|
-
// jev run the first ready job on the coordination layer's queue (
|
|
2
|
+
// jev run the first ready job on the coordination layer's queue (your TYPESAFE_KEY, else the free grades)
|
|
3
3
|
// jev run --job 8453:81100 a job you name (chain:id; ids like bitagent:8453:7287 work too)
|
|
4
4
|
// jev run --evidence pack.json your own pack, no layer at all: { subject, state, delivered?, checks?, priceUsdc? }
|
|
5
5
|
// jev run --job-file job.json one agent job: { id, task, criteria[], delivered, source?, checks[] } (see examples/jobs/)
|
|
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
|
|
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 (
|
|
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') ??
|
|
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' : 'no key: set TYPESAFE_KEY, or try --demo'} · record → ${network ===
|
|
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,
|
package/dist/ask.d.ts
CHANGED
|
@@ -6,9 +6,16 @@ export type Asked = {
|
|
|
6
6
|
answers: Record<string, Answer>;
|
|
7
7
|
model: string | null;
|
|
8
8
|
latency_ms: number | null;
|
|
9
|
-
connection: 'key';
|
|
10
|
-
trial:
|
|
9
|
+
connection: 'key' | 'trial';
|
|
10
|
+
trial: {
|
|
11
|
+
calls: number;
|
|
12
|
+
left: number;
|
|
13
|
+
} | null;
|
|
11
14
|
};
|
|
15
|
+
/** the coordination layer, where a caller without a TypeSafe key claims its free grades */
|
|
16
|
+
export declare const LAYER_URL = "https://coord.taifoon.dev";
|
|
17
|
+
/** the questions the layer's free grade answers (RUBRIC_v1 asks exactly these); another rubric needs the caller's own key */
|
|
18
|
+
export declare const LAYER_QUESTIONS: readonly string[];
|
|
12
19
|
export declare class JevError extends Error {
|
|
13
20
|
readonly status: number;
|
|
14
21
|
readonly next?: string | undefined;
|
|
@@ -22,3 +29,12 @@ export declare function askJev(a: {
|
|
|
22
29
|
model?: string;
|
|
23
30
|
fetch?: typeof fetch;
|
|
24
31
|
}): Promise<Asked>;
|
|
32
|
+
/** A free grade on the coordination layer (or one paid from a Taifoon key's balance, with `apiKey`): the layer asks Jev the
|
|
33
|
+
* four RUBRIC questions on `text` and returns the answers; this package composes the verdict from them as with a key. */
|
|
34
|
+
export declare function askLayer(a: {
|
|
35
|
+
text: string;
|
|
36
|
+
questions: readonly Question[];
|
|
37
|
+
layer?: string;
|
|
38
|
+
apiKey?: string | null;
|
|
39
|
+
fetch?: typeof fetch;
|
|
40
|
+
}): Promise<Asked>;
|
package/dist/ask.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
export const TYPESAFE_URL = 'https://api.typesafe.ai';
|
|
2
2
|
/** The model every key-path call asks for. Pinned, not `jev-latest`: a record says which model answered, and an alias can move. */
|
|
3
3
|
export const JEV_MODEL = 'jev-1.13.0';
|
|
4
|
+
/** the coordination layer, where a caller without a TypeSafe key claims its free grades */
|
|
5
|
+
export const LAYER_URL = 'https://coord.taifoon.dev';
|
|
6
|
+
/** the questions the layer's free grade answers (RUBRIC_v1 asks exactly these); another rubric needs the caller's own key */
|
|
7
|
+
export const LAYER_QUESTIONS = Object.freeze(['spec_met', 'unsupported_claim', 'ending', 'cheat_shaped']);
|
|
4
8
|
export class JevError extends Error {
|
|
5
9
|
status;
|
|
6
10
|
next;
|
|
@@ -33,3 +37,29 @@ export async function askJev(a) {
|
|
|
33
37
|
}
|
|
34
38
|
return { answers, model: j.model ?? null, latency_ms: Date.now() - started, connection: 'key', trial: null };
|
|
35
39
|
}
|
|
40
|
+
/** A free grade on the coordination layer (or one paid from a Taifoon key's balance, with `apiKey`): the layer asks Jev the
|
|
41
|
+
* four RUBRIC questions on `text` and returns the answers; this package composes the verdict from them as with a key. */
|
|
42
|
+
export async function askLayer(a) {
|
|
43
|
+
const f = a.fetch ?? fetch;
|
|
44
|
+
const started = Date.now();
|
|
45
|
+
const other = a.questions.map((q) => q.id).filter((id) => !LAYER_QUESTIONS.includes(id));
|
|
46
|
+
if (other.length)
|
|
47
|
+
throw new JevError(`the layer's free grade answers ${LAYER_QUESTIONS.join(', ')}; this rubric also asks ${other.join(', ')}: pass { key } (your TypeSafe key)`, 400, 'grade({ key: process.env.TYPESAFE_KEY })');
|
|
48
|
+
const base = (a.layer ?? LAYER_URL).replace(/\/$/, '');
|
|
49
|
+
if (!/^https:\/\//.test(base) && !/^http:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(base))
|
|
50
|
+
throw new JevError('the layer is only called over https', 400);
|
|
51
|
+
const r = await f(`${base}/v1/judge/compose`, { method: 'POST', headers: { 'content-type': 'application/json', 'x-taifoon-client': '@taifoon/jev', ...(a.apiKey ? { 'x-api-key': a.apiKey } : {}) }, body: JSON.stringify({ state: a.text }), signal: AbortSignal.timeout(65_000) });
|
|
52
|
+
const j = (await r.json().catch(() => ({})));
|
|
53
|
+
if (r.status === 402)
|
|
54
|
+
throw new JevError('no free grade left for this caller today: pass { key } (your TypeSafe key), a Taifoon key (layerKey), or pay one grade with x402 at POST ' + `${base}/v1/judge/compose`, 402, `${base}/v1/judge/compose`);
|
|
55
|
+
if (!r.ok)
|
|
56
|
+
throw new JevError(`the layer answered ${r.status}${j.error ? `: ${String(j.error).slice(0, 200)}` : ''}`, r.status);
|
|
57
|
+
const answers = {};
|
|
58
|
+
for (const q of a.questions) {
|
|
59
|
+
const x = j.receipt?.answers?.[q.id];
|
|
60
|
+
if (x)
|
|
61
|
+
answers[q.id] = norm(q.id, x);
|
|
62
|
+
}
|
|
63
|
+
const g = j.quota?.grades;
|
|
64
|
+
return { answers, model: j.model ?? null, latency_ms: Date.now() - started, connection: 'trial', trial: g && typeof g.free === 'number' && typeof g.left === 'number' ? { calls: g.free, left: g.left } : null };
|
|
65
|
+
}
|
package/dist/attest.d.ts
ADDED
|
@@ -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,223 @@
|
|
|
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: '0x8e9B9cE86a2d55c10318607b0c815B7B8C66254d', 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: null, answerLog: null, 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
|
+
const t = { name: `chain ${chainId}`, decisionLog: null, answerLog: null, stampRegistry: null, ...ATTEST_TARGETS[chainId], ...opts.targets?.[chainId] };
|
|
202
|
+
const gap = (contract) => missing.push({ chainId, network: t.name, contract, why: `no ${contract} on ${t.name} (${chainId}) in the address registry` });
|
|
203
|
+
// the answers first, then the decision that cites them (the order the layer's recorder sends)
|
|
204
|
+
if (input.answers) {
|
|
205
|
+
if (t.answerLog)
|
|
206
|
+
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 }]) });
|
|
207
|
+
else
|
|
208
|
+
gap('JevAnswerLog');
|
|
209
|
+
}
|
|
210
|
+
if (t.decisionLog)
|
|
211
|
+
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 }]) });
|
|
212
|
+
else
|
|
213
|
+
gap('JevDecisionLog');
|
|
214
|
+
if (verdict !== null) {
|
|
215
|
+
if (t.stampRegistry)
|
|
216
|
+
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 }]) });
|
|
217
|
+
else
|
|
218
|
+
gap('GradeStampRegistry');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const status = missing.length === 0 ? 'ready' : calls.length === 0 ? 'missing' : 'partial';
|
|
222
|
+
return { mark, recorded: true, networks, calls, missing, status };
|
|
223
|
+
}
|
package/dist/grade.d.ts
CHANGED
|
@@ -20,6 +20,11 @@ export type GradeInput = {
|
|
|
20
20
|
caller?: string;
|
|
21
21
|
/** base URL for `key` (default https://api.typesafe.ai) */
|
|
22
22
|
endpoint?: string;
|
|
23
|
+
/** with no `key` and no `answers`: the coordination layer whose free grades are claimed (default https://coord.taifoon.dev);
|
|
24
|
+
* false never calls it (then a key or answers are required) */
|
|
25
|
+
layer?: string | false;
|
|
26
|
+
/** a Taifoon key (tfr_…): a grade past the free ones is paid from its balance on the layer */
|
|
27
|
+
layerKey?: string | null;
|
|
23
28
|
fetch?: typeof fetch;
|
|
24
29
|
/** ms since epoch for the answer record; default now */
|
|
25
30
|
at?: number;
|
package/dist/grade.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// grade(): evidence → deterministic facts → Jev's atomic questions → composed verdict → receipt.
|
|
2
|
-
import { askJev, JevError } from './ask.js';
|
|
2
|
+
import { askJev, askLayer, JevError } from './ask.js';
|
|
3
3
|
import { answersOf, buildReceipt, DEFAULT_FACTS, inputFor, packOf, rubricOf } from './receipt.js';
|
|
4
4
|
export async function grade(g) {
|
|
5
5
|
const rubric = rubricOf(g.rubric);
|
|
@@ -17,9 +17,12 @@ export async function grade(g) {
|
|
|
17
17
|
connection = 'supplied';
|
|
18
18
|
}
|
|
19
19
|
else {
|
|
20
|
-
if (!g.key)
|
|
20
|
+
if (!g.key && g.layer === false)
|
|
21
21
|
throw new JevError('pass { key } (your TypeSafe key, from console.typesafe.ai) or { answers } you already have', 400);
|
|
22
|
-
|
|
22
|
+
// _FREE_GRADES_v1_: no key of your own → the caller's free grades on the coordination layer (then x402 or a Taifoon key)
|
|
23
|
+
asked = g.key
|
|
24
|
+
? await askJev({ text: jevState, questions: rubric.asked, key: g.key, endpoint: g.endpoint, fetch: g.fetch })
|
|
25
|
+
: await askLayer({ text: jevState, questions: rubric.asked, layer: g.layer || undefined, apiKey: g.layerKey, fetch: g.fetch });
|
|
23
26
|
const missing = rubric.asked.filter((q) => !asked.answers[q.id]).map((q) => q.id);
|
|
24
27
|
if (missing.length)
|
|
25
28
|
throw new JevError(`Jev returned no valid answer for: ${missing.join(', ')}`, 502);
|
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';
|
package/dist/pipeline.d.ts
CHANGED
|
@@ -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
|
|
112
|
-
|
|
113
|
-
* its digests only
|
|
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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// pipeline(): one job through the coordination layer's steps, each step a function with a typed output.
|
|
2
2
|
// pick → evidence → facts → grade → record → evaluator → premium → verify
|
|
3
3
|
// Every step works without the layer too: pass `evidence` (your own pack) and the layer is never read; `grade` then
|
|
4
|
-
// runs on your own TypeSafe key (or answers you already have), `record` returns unsigned calls, `premium` is priced from a `sellerRecord` you pass (or skipped). With the layer
|
|
4
|
+
// runs on your own TypeSafe key (or answers you already have, or without either on the caller's free grades on the layer), `record` returns unsigned calls, `premium` is priced from a `sellerRecord` you pass (or skipped). With the layer
|
|
5
5
|
// (https://coord.taifoon.dev by default) the job comes from /v1/judge/queue, the pack from /v1/judge/evidence, the
|
|
6
6
|
// seller's terms from /v1/pools/quote, and — only with a relayer key — the answers are recorded on the layer through
|
|
7
7
|
// /v1/judge/answers/record. Nothing here signs a transaction.
|
|
@@ -156,12 +156,17 @@ export async function pipeline(o = {}) {
|
|
|
156
156
|
}
|
|
157
157
|
else if (o.key)
|
|
158
158
|
g.key = o.key;
|
|
159
|
+
// _FREE_GRADES_v1_: no key → the caller's free grades on this layer (paid from relayerKey's balance past them); offline: none
|
|
160
|
+
else {
|
|
161
|
+
g.layer = layer ?? false;
|
|
162
|
+
g.layerKey = o.relayerKey ?? null;
|
|
163
|
+
}
|
|
159
164
|
// no key: a job the code checks reject is still graded (Jev is not asked); only asking Jev needs the key
|
|
160
165
|
try {
|
|
161
166
|
t.receipt = await grade(g);
|
|
162
167
|
}
|
|
163
168
|
catch (e) {
|
|
164
|
-
if (!o.key && !o.answers && /TypeSafe key/.test(e.message))
|
|
169
|
+
if (!o.key && !o.answers && !layer && /TypeSafe key/.test(e.message))
|
|
165
170
|
throw new Error('Jev needs your TypeSafe key: set TYPESAFE_KEY (console.typesafe.ai), or pass answers you already have');
|
|
166
171
|
throw e;
|
|
167
172
|
}
|
|
@@ -173,11 +178,11 @@ export async function pipeline(o = {}) {
|
|
|
173
178
|
await step('record', async () => {
|
|
174
179
|
if (!receipt.decision)
|
|
175
180
|
return { skip: `nothing to record: ${receipt.reasons[0] ?? receipt.verdict} (the facts decided; Jev was not asked)` };
|
|
176
|
-
t.recorded = await record(receipt, { network: o.network
|
|
181
|
+
t.recorded = await record(receipt, { network: o.network, networks: o.networks });
|
|
177
182
|
if (layer && o.relayerKey && receipt.answersRecord)
|
|
178
|
-
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') }) });
|
|
179
184
|
const rec = t.layerRecord?.recording;
|
|
180
|
-
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}
|
|
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') };
|
|
181
186
|
});
|
|
182
187
|
await step('evaluator', async () => {
|
|
183
188
|
if (receipt.verdict === 'needs_review')
|
package/dist/record.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { type
|
|
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
|
-
/**
|
|
14
|
-
|
|
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
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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 ?? '
|
|
19
|
-
const
|
|
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
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
{
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
}
|
package/dist/rpc-fallback.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// GENERATED by scripts/vendor-rpc.mjs from packages/rpc/fallback.json (measured 2026-10-
|
|
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/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
|
|
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.
|
|
16
|
-
|
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taifoon/jev",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
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.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jev",
|