@taifoon/jev 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +97 -191
  2. package/bin/jev.mjs +9 -3
  3. package/bin/run.mjs +86 -0
  4. package/bin/term.mjs +154 -0
  5. package/dist/ask.d.ts +5 -8
  6. package/dist/ask.js +16 -28
  7. package/dist/contracts.d.ts +5 -1
  8. package/dist/contracts.js +7 -3
  9. package/dist/grade.d.ts +0 -4
  10. package/dist/grade.js +5 -6
  11. package/dist/index.d.ts +7 -1
  12. package/dist/index.js +5 -1
  13. package/dist/job.d.ts +65 -0
  14. package/dist/job.js +50 -0
  15. package/dist/pipeline.d.ts +156 -0
  16. package/dist/pipeline.js +193 -0
  17. package/dist/rubric.js +1 -1
  18. package/dist/verify.d.ts +6 -0
  19. package/dist/verify.js +48 -9
  20. package/docs/API.md +20 -0
  21. package/docs/DEVELOP.md +7 -0
  22. package/docs/EVALUATORS.md +20 -0
  23. package/docs/ON_CHAIN.md +44 -0
  24. package/docs/PIPELINE.md +76 -0
  25. package/docs/RECORDS.md +10 -0
  26. package/docs/WORKFLOWS.md +38 -0
  27. package/examples/grade-job.mjs +17 -0
  28. package/examples/jobs/base-bitagent-7287.recorded.json +80 -0
  29. package/examples/jobs/invoice-extraction.json +24 -0
  30. package/examples/jobs/research-report-no-sources.json +29 -0
  31. package/examples/jobs/research-report.json +34 -0
  32. package/examples/jobs/signal-unsupported.json +27 -0
  33. package/examples/quickstart.mjs +22 -0
  34. package/package.json +19 -11
  35. package/workflows/{0.1.0 → 0.2.1}/README.md +3 -3
  36. package/workflows/{0.1.0 → 0.2.1}/jev-grader.json +2 -2
  37. package/workflows/{0.1.0 → 0.2.1}/manifest.json +12 -12
  38. package/workflows/{0.1.0 → 0.2.1}/schemas/answers.json +17 -35
  39. package/workflows/{0.1.0 → 0.2.1}/schemas/common.json +1 -1
  40. package/workflows/{0.1.0 → 0.2.1}/schemas/delivery.json +15 -31
  41. package/workflows/{0.1.0 → 0.2.1}/schemas/facts.json +15 -31
  42. package/workflows/{0.1.0 → 0.2.1}/schemas/hire.json +22 -45
  43. package/workflows/{0.1.0 → 0.2.1}/schemas/job.json +7 -15
  44. package/workflows/{0.1.0 → 0.2.1}/schemas/settlement.json +17 -33
  45. package/workflows/{0.1.0 → 0.2.1}/schemas/verdict.json +17 -35
  46. package/workflows/0.2.1/stamp-grade.json +175 -0
  47. package/workflows/0.1.0/stamp-grade.json +0 -175
  48. /package/workflows/{0.1.0 → 0.2.1}/batch-judge.json +0 -0
  49. /package/workflows/{0.1.0 → 0.2.1}/hire-judge-settle.json +0 -0
@@ -0,0 +1,193 @@
1
+ // pipeline(): one job through the coordination layer's steps, each step a function with a typed output.
2
+ // pick → evidence → facts → grade → record → evaluator → premium → verify
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 skipped. With the layer
5
+ // (https://coord.taifoon.dev by default) the job comes from /v1/judge/queue, the pack from /v1/judge/evidence, the
6
+ // seller's terms from /v1/pools/quote, and — only with a relayer key — the answers are recorded on the layer through
7
+ // /v1/judge/answers/record. Nothing here signs a transaction.
8
+ import { facts as runFacts } from './facts.js';
9
+ import { grade } from './grade.js';
10
+ import { record } from './record.js';
11
+ import { verify } from './verify.js';
12
+ import { evaluatorCall } from './evaluator/index.js';
13
+ export const LAYER = 'https://coord.taifoon.dev';
14
+ /** The steps in order, each with the layer route it reads (null: runs locally only). */
15
+ /** `required`: the later steps need its output, so saying no at the gate stops the run instead of skipping it. */
16
+ export const STEPS = Object.freeze([
17
+ { id: 'pick', title: 'Pick a job a grader is needed for', route: 'GET /v1/judge/queue', required: true },
18
+ { id: 'evidence', title: 'Read the evidence pack', route: 'GET /v1/judge/evidence/{chain}/{jobId}', required: true },
19
+ { id: 'facts', title: 'Code establishes the facts (a failed check ends the job)', route: null, required: true },
20
+ { id: 'grade', title: 'Jev answers RUBRIC_v1, code composes the verdict', route: null, required: true },
21
+ { id: 'record', title: 'Record the answers and the decision', route: 'POST /v1/judge/answers/record' },
22
+ { id: 'evaluator', title: 'The call that ends the job on its protocol', route: null },
23
+ { id: 'premium', title: 'Price the next premium from the seller record (Wilson)', route: 'POST /v1/pools/quote' },
24
+ { id: 'verify', title: 'Re-derive the receipt (and find it on chain)', route: null },
25
+ ]);
26
+ const num = (s) => { const m = /([0-9]+(?:\.[0-9]+)?)/.exec(s ?? ''); return m ? Number(m[1]) : null; };
27
+ const fact = (ev, label) => ev.facts.find(([k]) => label.test(k))?.[1];
28
+ /** A job reference: `8453:81100`, `8453:bitagent:8453:7287`, or a bare id (chain 8453). */
29
+ export function parseJob(j) {
30
+ if (typeof j === 'object')
31
+ return j;
32
+ const m = /^(\d+):(.+)$/.exec(j.trim());
33
+ return m ? { chainId: Number(m[1]), jobId: m[2] } : { chainId: 8453, jobId: j.trim() };
34
+ }
35
+ /** The layer's queue rows a grader can actually read: evidence must resolve (decimal id or label:chain:id). */
36
+ export const readable = (r) => /\/v1\/judge\/evidence\/\d+\/(\d+|[a-z0-9-]+:\d+:\d+)$/.test(r.evidence);
37
+ /**
38
+ * The deterministic facts an ERC-8183 evidence pack supports on its own:
39
+ * delivered = the pack names a delivery; submitted_before_deadline when both times are on chain; priceUsdc from "price".
40
+ * A protocol that can re-derive the delivery (a proof, a hash, a file) adds its own checks on top.
41
+ */
42
+ export function factsFromEvidence(ev, extra = {}) {
43
+ const delivered = fact(ev, /what was delivered/i);
44
+ const submitted = fact(ev, /^submitted$/i);
45
+ const deadline = fact(ev, /^deadline$/i);
46
+ const price = fact(ev, /^price$/i);
47
+ const checks = { ...extra };
48
+ if (submitted && deadline)
49
+ checks.submitted_before_deadline = !/after the deadline/i.test(submitted);
50
+ return { delivered: Boolean(delivered && !/^nothing|not delivered|no submission/i.test(delivered)), checks, priceUsdc: price && /usdc/i.test(price) ? num(price) : null };
51
+ }
52
+ /** The evaluator seat for a job id, when the id says which protocol it is on. */
53
+ export function protocolFor(jobId) {
54
+ const b = /^bitagent:\d+:(\d+)$/.exec(jobId);
55
+ if (b)
56
+ return { protocol: 'bitagent-erc8183', id: b[1] };
57
+ if (/^\d+$/.test(jobId))
58
+ return { protocol: 'virtuals-erc8183', id: jobId };
59
+ if (/^0x[0-9a-fA-F]{64}$/.test(jobId))
60
+ return { protocol: 'assurance-hook', id: jobId };
61
+ return null;
62
+ }
63
+ async function getJson(f, url, init) {
64
+ const r = await f(url, { ...init, signal: AbortSignal.timeout(30_000) });
65
+ const j = (await r.json().catch(() => ({})));
66
+ if (!r.ok || j.ok === false)
67
+ throw new Error(`${init?.method ?? 'GET'} ${url.replace(/^https?:\/\/[^/]+/, '')}: ${r.status} ${j.error ?? ''}`.trim());
68
+ return j;
69
+ }
70
+ export async function pipeline(o = {}) {
71
+ const f = o.fetch ?? fetch;
72
+ const layer = o.layer === false ? null : (o.layer ?? LAYER).replace(/\/$/, '');
73
+ if (!layer && !o.evidence)
74
+ throw new Error('independent mode (layer: false) needs `evidence`');
75
+ const t = { job: null, steps: [], receipt: null, recorded: null, layerRecord: null, evaluator: null, quote: null, verification: null };
76
+ let ev = null;
77
+ let budget = null;
78
+ let factsIn = null;
79
+ const step = async (id, run) => {
80
+ const s = STEPS.find((x) => x.id === id);
81
+ const t0 = Date.now();
82
+ let res;
83
+ if (o.before && !(await o.before(s, t)))
84
+ res = s.required ? { id, ok: true, stopped: true, skipped: 'stopped at the gate: the later steps need this one', ms: 0 } : { id, ok: true, skipped: 'skipped at the gate', ms: 0 };
85
+ else {
86
+ try {
87
+ const out = await run();
88
+ res = out && typeof out === 'object' && 'skip' in out ? { id, ok: true, skipped: out.skip, ms: Date.now() - t0 } : { id, ok: true, ms: Date.now() - t0, out };
89
+ }
90
+ catch (e) {
91
+ res = { id, ok: false, ms: Date.now() - t0, error: e.message };
92
+ }
93
+ }
94
+ t.steps.push(res);
95
+ await o.after?.(res, t);
96
+ };
97
+ const failed = () => t.steps.some((s) => !s.ok || s.stopped);
98
+ await step('pick', async () => {
99
+ if (o.evidence) {
100
+ t.job = { chainId: o.evidence.chainId ?? 0, jobId: o.evidence.subject, seller: o.evidence.seller ?? null };
101
+ return { skip: 'your own evidence pack' };
102
+ }
103
+ if (o.job) {
104
+ t.job = { ...parseJob(o.job), seller: null };
105
+ return t.job;
106
+ }
107
+ const q = await getJson(f, `${layer}/v1/judge/queue?limit=50`);
108
+ const row = q.rows.find(readable);
109
+ if (!row)
110
+ throw new Error('no queue row with a readable evidence pack');
111
+ t.job = { ...parseJob(`${/\/evidence\/(\d+)\//.exec(row.evidence)[1]}:${row.jobId}`), seller: row.seller };
112
+ budget = row.budget;
113
+ return { row, skipped_unreadable: q.rows.indexOf(row) };
114
+ });
115
+ if (failed())
116
+ return t;
117
+ await step('evidence', async () => {
118
+ if (o.evidence) {
119
+ ev = { chainId: 0, jobId: o.evidence.subject, state: o.evidence.state, facts: [] };
120
+ return { skip: 'your own evidence pack' };
121
+ }
122
+ ev = await getJson(f, `${layer}/v1/judge/evidence/${t.job.chainId}/${encodeURIComponent(t.job.jobId)}`);
123
+ t.job.seller ??= fact(ev, /^seller$/i) ?? null;
124
+ return { facts: ev.facts.length, gaps: ev.gaps ?? [], state_chars: ev.state.length };
125
+ });
126
+ if (failed() || !ev)
127
+ return t;
128
+ const pack = ev;
129
+ await step('facts', async () => {
130
+ factsIn = o.evidence ? { delivered: o.evidence.delivered ?? true, checks: o.evidence.checks ?? {}, priceUsdc: o.evidence.priceUsdc ?? null } : factsFromEvidence(pack);
131
+ budget ??= factsIn.priceUsdc ?? null;
132
+ return runFacts(factsIn);
133
+ });
134
+ if (failed())
135
+ return t;
136
+ await step('grade', async () => {
137
+ const g = { subject: o.subject ? o.subject(t.job) : { chainId: t.job.chainId, ref: t.job.jobId, ...(o.evidence?.label ? { label: o.evidence.label } : {}) }, evidence: pack.state, facts: runFacts(factsIn), fetch: f, ...(o.caller ? { caller: o.caller } : {}) };
138
+ if (o.answers) {
139
+ g.answers = o.answers;
140
+ if (o.model)
141
+ g.model = o.model;
142
+ }
143
+ else if (o.key)
144
+ g.key = o.key;
145
+ else
146
+ throw new Error('Jev needs your TypeSafe key: set TYPESAFE_KEY (console.typesafe.ai), or pass answers you already have');
147
+ t.receipt = await grade(g);
148
+ return { verdict: t.receipt.verdict, reasons: t.receipt.reasons, model: t.receipt.model, receiptHash: t.receipt.receiptHash };
149
+ });
150
+ if (failed() || !t.receipt)
151
+ return t;
152
+ const receipt = t.receipt;
153
+ await step('record', async () => {
154
+ if (!receipt.decision)
155
+ return { skip: `nothing to record: ${receipt.reasons[0] ?? receipt.verdict} (the facts decided; Jev was not asked)` };
156
+ t.recorded = await record(receipt, { network: o.network ?? 'none' });
157
+ if (layer && o.relayerKey && receipt.answersRecord)
158
+ 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' }) });
159
+ const rec = t.layerRecord?.recording;
160
+ return { status: t.recorded.status, recording: rec ? { used: rec.used ?? null, held: rec.held?.message ?? null } : null, calls: t.recorded.calls.map((c) => `${c.fn}@${c.chainId}${c.to ? '' : ' (no address)'}`), digests: t.recorded.digests, layer: t.layerRecord ?? (layer ? 'not written: pass a relayer key to record on the layer' : 'independent mode') };
161
+ });
162
+ await step('evaluator', async () => {
163
+ if (receipt.verdict === 'needs_review')
164
+ return { skip: 'needs_review ends nothing: the job goes to appeal' };
165
+ const p = o.protocol ? { protocol: o.protocol, id: t.job.jobId } : protocolFor(t.job.jobId);
166
+ if (!p)
167
+ return { skip: 'the job id does not say which protocol it is on (pass protocol)' };
168
+ try {
169
+ t.evaluator = evaluatorCall(p.protocol, p.id, receipt, receipt);
170
+ }
171
+ catch (e) {
172
+ return { skip: `${p.protocol}: ${e.message}` };
173
+ }
174
+ return t.evaluator ?? { skip: 'no evaluator call for this verdict' };
175
+ });
176
+ await step('premium', async () => {
177
+ const seller = t.job.seller;
178
+ if (!layer)
179
+ return { skip: 'independent mode: price it with @taifoon/jev-wilson (premium({ k, n }, { price }))' };
180
+ if (!seller)
181
+ return { skip: 'no seller address in the evidence' };
182
+ const price = o.priceUsdc ?? budget;
183
+ if (!price)
184
+ return { skip: 'no price for the job' };
185
+ t.quote = await getJson(f, `${layer}/v1/pools/quote`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ seller, price_usdc: price, chainId: 8453 }) });
186
+ return { guaranteed: t.quote.guaranteed, premium_ratio: t.quote.premium_ratio, premium_label: t.quote.premium_label, record: t.quote.record ?? null };
187
+ });
188
+ await step('verify', async () => {
189
+ t.verification = await verify(receipt, { chain: o.chainVerify ?? t.recorded?.status === 'sent', fetch: f });
190
+ return { ok: t.verification.ok, checks: t.verification.checks, problems: t.verification.problems };
191
+ });
192
+ return t;
193
+ }
package/dist/rubric.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // RUBRIC_v1: the questions Jev is asked, the thresholds, and the composition. Code decides the facts first; Jev answers
2
2
  // a few ATOMIC closed questions (never "is the job good?"); code composes the verdict from both under published
3
- // thresholds. Byte-for-byte the same rules as the Taifoon judge (judge/src/judge-compose.ts; test/parity.test.ts
3
+ // thresholds. Byte-for-byte the same rules as the hosted Taifoon judge (test/parity.test.ts
4
4
  // holds them together), so a receipt made here and one made by the hosted judge hash the same.
5
5
  import { sha256Hex } from './hash.js';
6
6
  const THRESHOLDS_V1 = Object.freeze({ completeSpecMin: 0.85, completeUnsupportedMax: 0.2, rejectSpecMax: 0.4, rejectUnsupportedMin: 0.7, cheatShapedMin: 0.5, autoCompleteCapUsdc: 50 });
package/dist/verify.d.ts CHANGED
@@ -32,9 +32,15 @@ export type Verification = {
32
32
  };
33
33
  problems: string[];
34
34
  };
35
+ /**
36
+ * `network`: 'devnet' (default, chain 36927) or 'base' (8453). On Base the answer row is found through
37
+ * JevAnswerLog.recordedAt (trusted records only) and the decision row in a window around it, because public Base
38
+ * endpoints refuse wide log ranges.
39
+ */
35
40
  export declare function verify(x: Receipt | string, opts?: {
36
41
  rpc?: string;
37
42
  fetch?: typeof fetch;
38
43
  rubric?: Rubric | RubricInput;
39
44
  chain?: boolean;
45
+ network?: 'devnet' | 'base';
40
46
  }): Promise<Verification>;
package/dist/verify.js CHANGED
@@ -9,19 +9,46 @@ const TOPIC_ANSWERED = keccakHex('JevAnswered(address,bytes32,bytes32,bytes32,by
9
9
  const TOPIC_DECIDED = keccakHex('Decided(bytes32,bytes32,address,uint256,bytes32,uint16,string,string)');
10
10
  const ANSWERED_DATA = ['address', 'bytes32', 'bytes32', 'string', 'string', 'uint256', 'bool'];
11
11
  const DECIDED_DATA = ['uint256', 'bytes32', 'uint16', 'string', 'string'];
12
- async function logs(rpc, f, address, topics, fromBlock) {
13
- const r = await f(rpc, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_getLogs', params: [{ address, topics, fromBlock: '0x' + fromBlock.toString(16), toBlock: 'latest' }] }), signal: AbortSignal.timeout(30_000) });
12
+ const hexN = (n) => '0x' + n.toString(16);
13
+ async function rpcCall(rpc, f, method, params) {
14
+ const r = await f(rpc, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }), signal: AbortSignal.timeout(30_000) });
14
15
  const j = (await r.json());
15
16
  if (j.error)
16
- throw new Error(`eth_getLogs: ${j.error.message}`);
17
- return j.result ?? [];
17
+ throw new Error(`${method}: ${j.error.message}`);
18
+ return j.result;
19
+ }
20
+ async function logs(rpc, f, address, topics, fromBlock, toBlock) {
21
+ return (await rpcCall(rpc, f, 'eth_getLogs', [{ address, topics, fromBlock: hexN(fromBlock), toBlock: toBlock == null ? 'latest' : hexN(toBlock) }])) ?? [];
22
+ }
23
+ const SEL_RECORDED_AT = keccakHex('recordedAt(bytes32)').slice(0, 10);
24
+ /** JevAnswerLog.recordedAt(digest): the block of the first trusted record, 0 when none. */
25
+ async function recordedAt(rpc, f, address, digest) {
26
+ const out = await rpcCall(rpc, f, 'eth_call', [{ to: address, data: SEL_RECORDED_AT + digest.slice(2) }, 'latest']);
27
+ return out && out !== '0x' ? Number(BigInt(out)) : 0;
28
+ }
29
+ /** eth_getLogs in windows of `step` blocks (public Base endpoints refuse wide ranges). */
30
+ async function logsWindowed(rpc, f, address, topics, from, to, step = 2000) {
31
+ const head = Number(BigInt(await rpcCall(rpc, f, 'eth_blockNumber', [])));
32
+ to = Math.min(to, head); // endpoints refuse a range past the head
33
+ const out = [];
34
+ for (let a = from; a <= to; a += step)
35
+ out.push(...(await logs(rpc, f, address, topics, a, Math.min(to, a + step - 1))));
36
+ return out;
18
37
  }
19
38
  const answerEvent = (l) => { const [recorder, inputDigest, decisionDigest, model, uri, , trusted] = decode(ANSWERED_DATA, l.data); return { tx: l.transactionHash, block: parseInt(l.blockNumber, 16), recorder: String(recorder), trusted: Boolean(trusted), subject: l.topics[2], inputDigest: inputDigest, decisionDigest: decisionDigest, model: String(model), uri: String(uri) }; };
20
39
  const decisionEvent = (l) => { const [index, digest, bps, model, uri] = decode(DECIDED_DATA, l.data); return { tx: l.transactionHash, block: parseInt(l.blockNumber, 16), recorder: `0x${String(l.topics[3]).slice(26)}`, index: Number(index), digest: digest, confidenceBps: Number(bps), model: String(model), uri: String(uri) }; };
40
+ /**
41
+ * `network`: 'devnet' (default, chain 36927) or 'base' (8453). On Base the answer row is found through
42
+ * JevAnswerLog.recordedAt (trusted records only) and the decision row in a window around it, because public Base
43
+ * endpoints refuse wide log ranges.
44
+ */
21
45
  export async function verify(x, opts = {}) {
46
+ const network = opts.network ?? 'devnet';
22
47
  const f = opts.fetch ?? fetch;
23
- const rpc = opts.rpc ?? CONTRACTS.devnet.rpc;
24
- const dev = CONTRACTS.devnet;
48
+ const rpc = opts.rpc ?? CONTRACTS[network].rpc;
49
+ const dev = network === 'base'
50
+ ? { chainId: CONTRACTS.base.chainId, answerLog: CONTRACTS.base.answerLog, decisionLog: CONTRACTS.base.decisionLog }
51
+ : { chainId: CONTRACTS.devnet.chainId, answerLog: CONTRACTS.devnet.answerLog, decisionLog: CONTRACTS.devnet.decisionLog };
25
52
  const checks = {};
26
53
  const problems = [];
27
54
  const onChain = { chainId: dev.chainId, answers: [], decisions: [] };
@@ -58,15 +85,27 @@ export async function verify(x, opts = {}) {
58
85
  }
59
86
  if (opts.chain !== false && answersDigest) {
60
87
  try {
61
- onChain.answers = (await logs(rpc, f, dev.answerLog.address, [TOPIC_ANSWERED, null, null, answersDigest], dev.answerLog.fromBlock)).map(answerEvent);
88
+ if (!dev.answerLog || !dev.decisionLog)
89
+ throw new Error(`this package version carries no ${network} address for the logs`);
90
+ if (network === 'base') {
91
+ const at = await recordedAt(rpc, f, dev.answerLog.address, answersDigest);
92
+ if (at > 0)
93
+ onChain.answers = (await logs(rpc, f, dev.answerLog.address, [TOPIC_ANSWERED, null, null, answersDigest], at, at)).map(answerEvent);
94
+ }
95
+ else
96
+ onChain.answers = (await logs(rpc, f, dev.answerLog.address, [TOPIC_ANSWERED, null, null, answersDigest], dev.answerLog.fromBlock)).map(answerEvent);
62
97
  // a bare digest: the answer row names its subject and decision, so the decision row can be found too
63
98
  const first = onChain.answers[0];
64
99
  if (!subjectId && first && !/^0x0{64}$/.test(first.decisionDigest)) {
65
100
  subjectId = first.subject;
66
101
  decision = first.decisionDigest;
67
102
  }
68
- if (subjectId && decision)
69
- onChain.decisions = (await logs(rpc, f, dev.decisionLog.address, [TOPIC_DECIDED, subjectId], dev.decisionLog.fromBlock)).map(decisionEvent).filter((d) => d.digest.toLowerCase() === decision.toLowerCase());
103
+ if (subjectId && decision) {
104
+ const found = network === 'base' && first
105
+ ? await logsWindowed(rpc, f, dev.decisionLog.address, [TOPIC_DECIDED, subjectId], Math.max(dev.decisionLog.fromBlock, first.block - 4000), first.block + 4000)
106
+ : await logs(rpc, f, dev.decisionLog.address, [TOPIC_DECIDED, subjectId], dev.decisionLog.fromBlock);
107
+ onChain.decisions = found.map(decisionEvent).filter((d) => d.digest.toLowerCase() === decision.toLowerCase());
108
+ }
70
109
  }
71
110
  catch (e) {
72
111
  problems.push(`chain read failed: ${e instanceof Error ? e.message : String(e)}`);
package/docs/API.md ADDED
@@ -0,0 +1,20 @@
1
+ # API
2
+
3
+ ## The five calls
4
+
5
+ | Call | What it does | Network |
6
+ |---|---|---|
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
+ | `facts({ delivered, checks, priceUsdc? })` | Runs the checks your protocol supplies. A check is a value or an (async) function; one that throws counts as "could not be checked". | None |
9
+ | `record(receipt, { network, send? })` | Returns the unsigned `JevAnswerLog.record` and `JevDecisionLog.record` calls. `network` is a flag: `none`, `devnet` (default), `base` or `both`. Pass `send` (your wallet) to send every call that has an address. | None unless you pass `send` |
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
+ | `verify(receiptOrDigest)` | Recomputes every digest, re-composes the verdict and finds the `JevAnswered` and `Decided` events. | One devnet RPC read |
12
+
13
+ It also exports `RUBRIC_v1` and `CONTRACTS`. Nothing else.
14
+
15
+ **Where Jev is reached.** Calls go to `https://api.typesafe.ai/v1/systemone` with your own TypeSafe key from
16
+ [console.typesafe.ai](https://console.typesafe.ai), and to no one else. The key is not stored and never appears in a receipt. If you already
17
+ have the answers (from the n8n node, for example), pass `answers` and nothing is asked.
18
+
19
+ **Your own rubric.** `grade({ rubric: { version, questions, asked?, thresholds?, compose? } })`. The rubric hash is
20
+ sha256 of its questions and goes into the receipt.
@@ -0,0 +1,7 @@
1
+ # Develop
2
+
3
+ ```
4
+ npm ci && npm run check # typecheck, tests (golden runs, recorded calldata, hashes vs viem), build
5
+ node scripts/vendor-n8n.mjs # refresh the n8n node's copy of the core; test/vendor.test.ts fails when it drifts
6
+ npm run bundle && npm run zip # rebuild workflows/<version>/ and zip it for the release
7
+ ```
@@ -0,0 +1,20 @@
1
+ # Evaluator seats
2
+
3
+ Each adapter is one small file with its ABI fragment. Each one is tested against calldata that a mined transaction
4
+ carried (`test/fixtures/evaluator-calls.json`, written by `scripts/record-evaluator-fixtures.mjs`).
5
+
6
+ | `protocol` | Call | Tested against |
7
+ |---|---|---|
8
+ | `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)) |
9
+ | `judge-adapter` | `postVerdict(bytes32 jobId, 1\|2, 2, bytes32 digest)` → hook complete / reject | devnet [`0x07853069…2012`](https://www.taifoon.io/scan/36927/tx/0x07853069cf43200fad3761d05b532c654df6f02b8a47f5f767bec18cb91a2012) |
10
+ | `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 |
11
+ | `virtuals-memo-acp` | ACPRouter `signMemo(uint256 memoId, bool approved, string reason)` on the evaluation memo | Base [`0x94ba6b49…622e`](https://basescan.org/tx/0x94ba6b497e2fc2ecf7ae256e1a490cabdc4a5357ed306ee2071f46a1c4ef622e) |
12
+ | `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) |
13
+
14
+ The digest in the call is the receipt's decision digest. That is the same 32 bytes JevDecisionLog holds, so the job's
15
+ ending, the decision record and the receipt all carry one value. On a hard fail no decision exists, so pass
16
+ `receipt.receiptHash`.
17
+
18
+ Whoever holds the seat signs the call; this package never does. The BitAgent implementation is unverified on Base, and
19
+ its layout was read from its own transactions. Its jobs settle today through the platform Evaluator, which only
20
+ BitAgent's operators can call.
@@ -0,0 +1,44 @@
1
+ # On chain
2
+
3
+ The logs are on the public devnet (chain 36927, free gas from `https://faucet.taifoon.dev`). Any account may send to
4
+ them.
5
+
6
+ | Contract | Address | Rule |
7
+ |---|---|---|
8
+ | JevAnswerLog | `0xD8c1d8188Fc8EE388792f6EB3B3dbd4f341Ba8e3` | Append-only, no owner. Recorders fixed at deploy are `trusted`; any other sender is logged with `trusted = false` |
9
+ | JevDecisionLog | `0x1D622511862DD7DEffB8fEeBc225E0FAdA1e9A05` | Append-only, no owner, no upgrade. The recorder is `msg.sender` |
10
+
11
+ On Base (8453), since 0.2.0:
12
+
13
+ | Contract | Address | Rule |
14
+ |---|---|---|
15
+ | JevAnswerLog | `0x8e9B9cE86a2d55c10318607b0c815B7B8C66254d` | Same bytecode. The only `trusted` recorder is `0xe9F0E71e7Fc66864126C0aE5588a7858b25dE51D` |
16
+ | JevDecisionLog | `0x209490d6A0FFC5368A42b0c2208BDCda853f6a92` | Same bytecode. The recorder is `msg.sender` |
17
+
18
+ `record(receipt, { network: 'base' })` returns the Base calls with `to` set. `verify(digest, { network: 'base' })` and
19
+ `npx @taifoon/jev verify <digest> --network base` read them (the CLI's default `any` tries the devnet, then Base).
20
+ 0.1.0 carried no Base addresses. An earlier Base answer log, `0x5bac70eb78224bbCBa83f5A36DF09D549d3f57fa`, is superseded and holds no records.
21
+
22
+ Here is one real grade, end to end: n8n execution 86, a proof-verification job graded with RUBRIC_v1.
23
+
24
+ - Answers recorded in devnet tx
25
+ [`0x9a38cf55…a61b`](https://www.taifoon.io/scan/36927/tx/0x9a38cf5541bb82a8a6e2e5cf1a96ed25fdc46921c351506da83023f5dec1a61b).
26
+ The answers digest is `0x6c7e6d2977689f1c9639a377b165f998d1160cc30eca8ff4f1fedc793db2f52d`.
27
+ - Decision anchored in devnet tx
28
+ [`0x687ba9d7…4ee4`](https://www.taifoon.io/scan/36927/tx/0x687ba9d73d5789a2df9d9d09e94b6ed005447d7f5ece97f9226ea0d51f514ee4).
29
+ Decision digest `0x781b0fce…3667`, confidence 2,700 bps.
30
+ - Jev answered spec_met `{ yes: 0.36, no: 0.64 }`, so the composed verdict is **reject** (spec_met ≤ 0.40).
31
+
32
+ `verify('0x6c7e6d29…f52d')` finds both rows. This is the real output:
33
+
34
+ ```
35
+ {"ok":true,"checks":{"answersOnChain":true,"decisionOnChain":true},
36
+ "answer":{"tx":"0x9a38cf5541bb82a8a6e2e5cf1a96ed25fdc46921c351506da83023f5dec1a61b","trusted":true},
37
+ "decision":{"tx":"0x687ba9d73d5789a2df9d9d09e94b6ed005447d7f5ece97f9226ea0d51f514ee4","confidenceBps":2700}}
38
+ ```
39
+
40
+ The tests re-derive these rows offline, byte for byte:
41
+
42
+ - **Execution 86:** the answers digest, the `JevAnswerLog.record` calldata (equal to the mined input) and the decision
43
+ digest.
44
+ - **Execution 88:** run through `grade()` itself, it lands on the anchored decision digest and composes needs_review.
@@ -0,0 +1,76 @@
1
+ # How grading works
2
+
3
+ ## What Jev is
4
+
5
+ Jev is TypeSafe's System One decision model. When this was written it answered as **`jev-1.13.0`**. You don't ask Jev
6
+ for prose. You give it closed questions, and every answer comes back as a **full probability distribution** over the
7
+ options, plus a confidence value. An answer to "did the delivery meet the spec?" looks like `{ yes: 0.93, no: 0.07 }`,
8
+ not a paragraph.
9
+
10
+ This package puts Jev inside a fixed judging procedure:
11
+
12
+ - Code establishes the facts first. A failed check ends the job, and Jev is never asked.
13
+ - Jev answers four **atomic** questions from a published rubric. It never gets a holistic "is the job good?".
14
+ - Code composes the verdict from the facts and the answers under published thresholds.
15
+ - Everything is hashed into a receipt and turned into the one call that ends the job. Recording it on chain is
16
+ optional: `record()` builds the calls only when you ask.
17
+
18
+ How to describe the result: *graded against a published rubric by a pinned decision model, with an appeal.* It is not
19
+ "independently verified". An on-chain record is an attestation by whoever sent it, not a proof that the answer is right.
20
+
21
+ ## The pipeline
22
+
23
+ ```
24
+ evidence ─► facts() ──── a check failed? ── yes ─► reject (hard fail; Jev is not asked)
25
+ code checks │
26
+ no
27
+ ▼
28
+ Jev reads: evidence (cut at 3,600 chars) + the facts section + one instruction
29
+ Jev answers RUBRIC_v1: spec_met · unsupported_claim · ending · cheat_shaped
30
+ ▼
31
+ compose under THRESHOLDS_v1 ─► complete | reject | needs_review
32
+ ▼
33
+ receipt: rubric hash, input digest, every distribution, verdict, reasons, receiptHash
34
+ ▼
35
+ record() ─► JevAnswerLog.record (jev.answer.v1) + JevDecisionLog.record (decision.v2)
36
+ ▼
37
+ evaluatorCall() ─► complete / reject on the job's protocol (needs_review ends nothing: appeal)
38
+ ```
39
+
40
+ THRESHOLDS_v1:
41
+
42
+ - **complete** needs spec_met ≥ 0.85, unsupported_claim ≤ 0.20 and ending = complete.
43
+ - **reject** follows from spec_met ≤ 0.40, unsupported_claim ≥ 0.70 or ending = reject.
44
+ - **needs_review** is the result when cheat_shaped ≥ 0.50 (escalate, never slash on this alone) or when the answers
45
+ land in the mid band.
46
+ - Above 50 USDC a complete is held for review.
47
+
48
+ ## Run it: `npx @taifoon/jev run`
49
+
50
+ One job through the coordination layer's steps, one step at a time. Each step shows the layer route it reads and
51
+ what came back, and asks before it runs (`--yes` runs them all; `--json` prints the whole trace).
52
+
53
+ ```
54
+ npx @taifoon/jev run # the first job on /v1/judge/queue whose evidence the layer can read
55
+ npx @taifoon/jev run --job 8453:bitagent:8453:7287 --network both
56
+ npx @taifoon/jev run --no-layer --evidence pack.json # your own pack, no layer at all: { subject, state, checks? }
57
+ ```
58
+
59
+ | # | Step | Layer route | Without the layer |
60
+ |---|---|---|---|
61
+ | 1 | pick a job a grader is needed for | `GET /v1/judge/queue` | your `--evidence` |
62
+ | 2 | read the evidence pack | `GET /v1/judge/evidence/{chain}/{jobId}` | your `--evidence` |
63
+ | 3 | code establishes the facts | none | the checks in your pack |
64
+ | 4 | Jev answers RUBRIC_v1, code composes the verdict | none | same |
65
+ | 5 | record the answers and the decision | `POST /v1/judge/answers/record` (only with `TAIFOON_RELAYER_KEY`) | unsigned calls |
66
+ | 6 | the evaluator call that ends the job | none | same |
67
+ | 7 | price the next premium from the seller record | `POST /v1/pools/quote` | skipped; use `@taifoon/jev-wilson` |
68
+ | 8 | re-derive the receipt (and find it on chain) | none | same |
69
+
70
+ Steps 1 to 4 are required: saying no at one stops the run. Keys come from the environment only, `TYPESAFE_KEY` (your own
71
+ TypeSafe key; without it the Jev step stops and says where to get one) and `TAIFOON_RELAYER_KEY`, and are never printed. Nothing is signed. In code it is `pipeline()`,
72
+ with a `before` gate and an `after` hook per step.
73
+
74
+ Real run (2026-09-27): BitAgent job 7287 on Base asked for "equity_research where ticker is 'AAPL'" and
75
+ got a 32-byte digest. Facts passed (delivered, before the deadline, 1.5 USDC). Jev answered spec_met 0.03, so the
76
+ verdict is **reject**; the receipt re-derives; the seller's quote is 0.0%–39.0% (6 settled, thin record).
@@ -0,0 +1,10 @@
1
+ # The records
2
+
3
+ - **Receipt.** `receiptHash = sha256(JSON.stringify(body))`. The body holds the rubric and its hash, the thresholds, the
4
+ subject, `stateHash`, the facts, the model, every answer with its distribution, what was not asked, the verdict,
5
+ `auto`, `forced`, the reasons and the scores.
6
+ - **decision.v2.** `digest = sha256(canonical { v, kind, subject{chainId, at, ref}, model, answers[id, question,
7
+ options, value, probabilities, confidence], input_digest })`, with keys sorted.
8
+ `subjectId = keccak256(abi.encode("taifoon.decision.subject.v1", chainId, at, ref))`.
9
+ - **jev.answer.v1.** `digest = sha256(canonical record)`, covering questions, answers with distributions, model,
10
+ upstream model, latency, credential path (`caller-credential`; records made before 0.2.0 may say `trial`), caller and time.
@@ -0,0 +1,38 @@
1
+ # Workflows and n8n
2
+
3
+ ## In n8n
4
+
5
+ The TypeSafe node (`@taifoon/n8n-nodes-typesafe` 1.5.0) carries this package's network-free core under **Jev Options**:
6
+
7
+ - **Ask RUBRIC_v1** routes Pass = complete, Fail = reject and Review = needs_review.
8
+ - **Facts (JSON)** takes checks you already made. A false check rejects without asking Jev.
9
+ - **Record On** takes `none`, `devnet`, `base` or `both`, and **Evaluator Call** takes a protocol and a job id.
10
+
11
+ The node outputs the receipt and the unsigned calls, and it signs nothing. With the options unset it behaves exactly as
12
+ 1.4.0.
13
+
14
+ ## Workflows
15
+
16
+ `workflows/0.2.1/` holds the four n8n workflows that run Jev. The same files are attached to the
17
+ GitHub release as `jev-workflows-0.2.1.zip`:
18
+
19
+ - **`hire-judge-settle.json`** runs hire → prepare (facts) → TypeSafe (the four RUBRIC_v1 questions) → answers
20
+ (verdict, recorded and anchored) → settle on devnet. Executions 86 and 88 of this workflow are the golden runs above.
21
+ - **`jev-grader.json`** grades Base jobs nobody ruled on, then stamps each verdict on the devnet GradeStampRegistry.
22
+ - **`batch-judge.json`** runs prepare → Jev → answers for each subject, then `POST /v1/judge/batch`.
23
+ - **`stamp-grade.json`** takes a recorded decision, turns it into the canonical verdict and stamps it.
24
+
25
+ Credential references are placeholders: `{ "id": "", "name": "REPLACE: <credential type>" }`. After import, pick your
26
+ own credential of that type. Each step names the entity it takes and emits by `$id` (`meta.taifoon.steps`). The schemas
27
+ behind those `$id`s are in `workflows/0.2.1/schemas/`. `manifest.json` pins every file by sha256.
28
+ `workflows/0.2.1/README.md` explains each workflow step by step and lists the nodes and credentials it needs:
29
+
30
+ - `@taifoon/n8n-nodes-typesafe`, tested with 1.3.0, is on npm.
31
+ - `n8n-nodes-taifoon`, tested with 0.4.2, and `n8n-nodes-taifoon-devnet-signer`, tested with 0.2.0, are custom
32
+ extensions and are not on npm.
33
+
34
+ ```
35
+ npx @taifoon/jev workflows list
36
+ npx @taifoon/jev workflows export ./jev-workflows # workflows + schemas + manifest + README
37
+ npx @taifoon/jev verify 0x6c7e6d2977689f1c9639a377b165f998d1160cc30eca8ff4f1fedc793db2f52d
38
+ ```
@@ -0,0 +1,17 @@
1
+ // Grade one agent job: did the agent do the work as the task laid it out?
2
+ // TYPESAFE_KEY=… node examples/grade-job.mjs examples/jobs/research-report.json
3
+ // node examples/grade-job.mjs examples/jobs/research-report-no-sources.json (code rejects it; Jev is not asked)
4
+ // Your key comes from console.typesafe.ai. It is sent to TypeSafe only, and never stored or recorded.
5
+ import { readFileSync } from 'node:fs';
6
+ import { prepareJob, facts, grade, record } from '@taifoon/jev';
7
+
8
+ const spec = JSON.parse(readFileSync(process.argv[2] ?? new URL('./jobs/research-report.json', import.meta.url), 'utf8'));
9
+ const job = prepareJob(spec); // task + numbered criteria + source + delivery, and the code checks
10
+
11
+ const f = await facts(job.facts);
12
+ console.log('facts', f.checks, f.checksOk === false ? '→ a check failed: code rejects, Jev is not asked' : '');
13
+
14
+ const receipt = await grade({ subject: job.subject, evidence: job.evidence, facts: f, key: process.env.TYPESAFE_KEY });
15
+ console.log(receipt.verdict, '·', receipt.reasons.join(' · '));
16
+ if (receipt.answers) for (const [id, a] of Object.entries(receipt.answers)) console.log(' ', id.padEnd(18), a.value, a.probabilities);
17
+ if (receipt.decision) console.log('record:', (await record(receipt)).calls.map((c) => c.fn).join(' + '));
@@ -0,0 +1,80 @@
1
+ {
2
+ "_about": "A real job on Base, graded by Jev (jev-1.13.0) through the Taifoon coordination layer on 2026-09-27. Everything here is copied from the recorded decision; test/jobs.test.ts re-derives its digests offline.",
3
+ "job": {
4
+ "chainId": 8453,
5
+ "protocol": "BitAgent ERC-8183",
6
+ "contract": "0x5009ABB3A309115a4a682C66BAf3BC9E0329BaB7",
7
+ "jobId": "7287",
8
+ "ref": "bitagent:8453:7287",
9
+ "task": "equity_research where ticker is 'AAPL'",
10
+ "price": "1.5 USDC",
11
+ "delivered": "a 32-byte digest; the content behind it is not published on chain",
12
+ "evidence_route": "https://coord.taifoon.dev/v1/judge/evidence/8453/bitagent:8453:7287"
13
+ },
14
+ "subject": {
15
+ "chainId": 8453,
16
+ "ref": "bitagent:8453:7287",
17
+ "label": "RUBRIC_v1 · bitagent:8453:7287"
18
+ },
19
+ "evidence": "job: ERC-8183 job 7287 of the BitAgent deployment on chain 8453 (contract 0x5009abb3a309115a4a682c66baf3bc9e0329bab7; observatory id bitagent:8453:7287)\nwhat the buyer asked (job description on chain): equity_research where ticker is 'AAPL\nprice: 1.5 USDC funded into escrow\nbuyer: 0xc41c35cd98ed94b54a2adfcdf24a5574fdd0b5b8\nseller: 0x1112889be806840a66419ea1e8d4dd21852993e5\nevaluator: 0x4302e523d982f3b89cfc43ce4530c012b495ec11\nevaluator kind: platform (bitagent_platform_evaluator) — buyer = the client evaluates its own job; none = no evaluator, the job auto-completes; named = another address; platform = the deployment's own evaluator contract, settled by its operator through forceSettleJob\nhook: 0x4302e523d982f3b89cfc43ce4530c012b495ec11\ncreated: 2026-08-28 05:51:23 UTC\nfunded: 2026-08-28 05:51:43 UTC (20 s after creation)\ndeadline: 2026-09-27 05:51:16 UTC (30.0 d after funding)\nsubmitted: 2026-08-28 05:51:55 UTC (12 s after funding), 30.0 d before the deadline\nwhat was delivered: a 32-byte digest 0xea9708d1352f28825bce3055ff248dbdae50314094f2adf3526bfe113d3f0641; the content behind it is not published on chain\nhow it ended: still open\nevents in order: JobCreated (block 50553468) → BudgetSet (block 50553473) → ProviderSet (block 50553475) → JobFunded (block 50553478) → JobSubmitted (block 50553484)\nthis buyer with this seller: 6 jobs: 6 open\nseller record (paid jobs that ended): 6 completed and paid, 0 delivered then rejected or paid and never delivered, 0 paid and expired after delivery with no ruling; 148 jobs in all\nseller speed: median 10 s from funding to delivery over 66 paid jobs\nseller buyers: 10 distinct buyers; the largest placed 7% of its jobs\nnot known:\n- the delivered content itself is not on chain, only its digest",
20
+ "facts": {
21
+ "delivered": true,
22
+ "checks": {
23
+ "funded": true,
24
+ "submitted": true
25
+ }
26
+ },
27
+ "model": "jev-1.13.0",
28
+ "answers": [
29
+ {
30
+ "id": "spec_met",
31
+ "value": "no",
32
+ "confidence": 0.95,
33
+ "probabilities": {
34
+ "no": 0.97,
35
+ "yes": 0.03
36
+ }
37
+ },
38
+ {
39
+ "id": "unsupported_claim",
40
+ "value": "no",
41
+ "confidence": 0.87,
42
+ "probabilities": {
43
+ "no": 0.94,
44
+ "yes": 0.06
45
+ }
46
+ },
47
+ {
48
+ "id": "ending",
49
+ "value": "needs_review",
50
+ "confidence": 0.9,
51
+ "probabilities": {
52
+ "complete": 0.07,
53
+ "expire": 0,
54
+ "needs_review": 0.93,
55
+ "reject": 0
56
+ }
57
+ },
58
+ {
59
+ "id": "cheat_shaped",
60
+ "value": "no",
61
+ "confidence": 0.13,
62
+ "probabilities": {
63
+ "no": 0.56,
64
+ "yes": 0.44
65
+ }
66
+ }
67
+ ],
68
+ "recorded": {
69
+ "decision": "decision-1790520028322-8a6fc06cd8",
70
+ "digest": "0x8a6fc06cd8bcbfc0ec4905a1b517a6840fd91312b291c9d4d4bfca88d6a4ed1b",
71
+ "input_digest": "0x4a66b95eab6227f134aafbf58a1d8a2a5388267007ae2941c4c883e25c3ecc1f",
72
+ "subject_id": "0x3d7648d403fc2475c772d92c7a0a3a5a6750a5544307895771b83f4637752923",
73
+ "confidence_bps": 1300,
74
+ "anchor": {
75
+ "chainId": 36927,
76
+ "contract": "0x1D622511862DD7DEffB8fEeBc225E0FAdA1e9A05",
77
+ "tx": "0xffc608f8c8b39ff6992e8ec8d589b8784a3cf4199359c408f8eed1d4b581c622"
78
+ }
79
+ }
80
+ }