@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,24 @@
1
+ {
2
+ "_note": "ILLUSTRATIVE job written by hand to show the data a grader needs. Not a real delivery.",
3
+ "id": "example:invoice-extraction",
4
+ "task": "Extract the invoice into JSON with invoice_no, total, currency and due_date",
5
+ "criteria": [
6
+ "Returns one JSON object with exactly the four fields",
7
+ "Every value matches the source invoice",
8
+ "due_date is ISO 8601 (YYYY-MM-DD)"
9
+ ],
10
+ "source": "INVOICE INV-20931\nIssued 2026-09-01 by Northwind Parts GmbH\nTotal due: EUR 1,240.50\nPayment due by 2026-10-01",
11
+ "delivered": "{\"invoice_no\": \"INV-20931\", \"total\": 1240.50, \"currency\": \"EUR\", \"due_date\": \"2026-10-01\"}",
12
+ "checks": [
13
+ {
14
+ "kind": "json",
15
+ "keys": [
16
+ "invoice_no",
17
+ "total",
18
+ "currency",
19
+ "due_date"
20
+ ]
21
+ }
22
+ ],
23
+ "priceUsdc": 0.2
24
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "_note": "ILLUSTRATIVE job written by hand to show the data a grader needs. Not a real delivery.",
3
+ "id": "example:research-report-no-sources",
4
+ "chainId": 8453,
5
+ "task": "equity_research where ticker is 'AAPL'",
6
+ "criteria": [
7
+ "Names the ticker AAPL",
8
+ "Lists at least three risks, each with one sentence of reasoning",
9
+ "Ends with one call: BUY, HOLD or SELL",
10
+ "Cites at least two public sources by link",
11
+ "States no figure it does not source"
12
+ ],
13
+ "delivered": "AAPL looks strong. Risks: China, regulation, supply chain. Call: BUY.",
14
+ "checks": [
15
+ {
16
+ "kind": "includes",
17
+ "terms": [
18
+ "AAPL",
19
+ "BUY"
20
+ ],
21
+ "name": "names the ticker and a call"
22
+ },
23
+ {
24
+ "kind": "sources",
25
+ "min": 2
26
+ }
27
+ ],
28
+ "priceUsdc": 1.5
29
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "_note": "ILLUSTRATIVE job written by hand to show the data a grader needs. Not a real delivery.",
3
+ "id": "example:research-report",
4
+ "chainId": 8453,
5
+ "task": "equity_research where ticker is 'AAPL'",
6
+ "criteria": [
7
+ "Names the ticker AAPL",
8
+ "Lists at least three risks, each with one sentence of reasoning",
9
+ "Ends with one call: BUY, HOLD or SELL",
10
+ "Cites at least two public sources by link",
11
+ "States no figure it does not source"
12
+ ],
13
+ "delivered": "AAPL equity research, 26 September 2026.\nPrice context: AAPL closed at the level shown on the source page linked below; this report does not restate a price it cannot cite.\nRisks:\n1. China demand: a slowdown in the Greater China segment would hit iPhone revenue first.\n2. Regulation: app store rules in the EU and the US could cut services margins.\n3. Supply chain: assembly is concentrated in a few regions.\nCall: HOLD. The three risks balance the services growth described in the latest filing.\nSources:\n- https://investor.apple.com/sec-filings/default.aspx\n- https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK=0000320193",
14
+ "checks": [
15
+ {
16
+ "kind": "includes",
17
+ "terms": [
18
+ "AAPL",
19
+ "HOLD"
20
+ ],
21
+ "name": "names the ticker and a call"
22
+ },
23
+ {
24
+ "kind": "sources",
25
+ "min": 2
26
+ },
27
+ {
28
+ "kind": "deadline",
29
+ "submittedAt": "2026-09-26T10:00:00Z",
30
+ "deadline": "2026-09-27T00:00:00Z"
31
+ }
32
+ ],
33
+ "priceUsdc": 1.5
34
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "_note": "ILLUSTRATIVE job written by hand to show the data a grader needs. Not a real delivery.",
3
+ "id": "example:signal-unsupported",
4
+ "chainId": 8453,
5
+ "task": "Give me the current oracle signal for BTC with its EV score and the data it rests on",
6
+ "criteria": [
7
+ "Gives one signal: LONG, SHORT or FLAT",
8
+ "Gives an EV score",
9
+ "Shows the data the score rests on, by link"
10
+ ],
11
+ "delivered": "BTC signal: LONG. EV score 0.87, backtested win rate 91% over 5 years, guaranteed edge. Data: https://example.com/btc",
12
+ "checks": [
13
+ {
14
+ "kind": "includes",
15
+ "terms": [
16
+ "BTC",
17
+ "LONG"
18
+ ],
19
+ "name": "gives a signal"
20
+ },
21
+ {
22
+ "kind": "sources",
23
+ "min": 1
24
+ }
25
+ ],
26
+ "priceUsdc": 1
27
+ }
@@ -0,0 +1,22 @@
1
+ // Did the agent do the work it was hired for, as the task laid it out? One job, graded end to end.
2
+ // TYPESAFE_KEY=… node examples/quickstart.mjs (your own key, from console.typesafe.ai)
3
+ import { prepareJob, facts, grade, record, evaluatorCall, verify } from '@taifoon/jev';
4
+
5
+ // 1. The job as the buyer laid it out, and what the agent handed in (illustrative).
6
+ const job = prepareJob({
7
+ id: 'job-4711', chainId: 8453,
8
+ task: 'Extract the invoice into JSON with invoice_no, total, currency and due_date',
9
+ criteria: ['Returns one JSON object with exactly the four fields', 'Every value matches the source invoice', 'due_date is ISO 8601'],
10
+ source: 'INVOICE INV-20931 · Total due: EUR 1,240.50 · Payment due by 2026-10-01',
11
+ delivered: '{"invoice_no":"INV-20931","total":1240.50,"currency":"EUR","due_date":"2026-10-01"}',
12
+ checks: [{ kind: 'json', keys: ['invoice_no', 'total', 'currency', 'due_date'] }], // code checks first
13
+ });
14
+
15
+ // 2. Code states the facts; Jev answers the rubric about the delivery against the criteria and the source.
16
+ const receipt = await grade({ subject: job.subject, evidence: job.evidence, facts: facts(job.facts), key: process.env.TYPESAFE_KEY });
17
+ console.log(receipt.verdict, '·', receipt.reasons[0]);
18
+
19
+ // 3. The record and the call that ends the job, both unsigned: whoever holds the seat signs.
20
+ const onchain = await record(receipt);
21
+ const end = evaluatorCall('virtuals-erc8183', 4711n, receipt, receipt);
22
+ console.log(onchain.calls.map((c) => c.fn), '→', end?.fn, '· verifies:', (await verify(receipt, { chain: false })).ok);
package/package.json CHANGED
@@ -1,18 +1,24 @@
1
1
  {
2
2
  "name": "@taifoon/jev",
3
- "version": "0.1.0",
4
- "description": "Jev as an on-chain grader for any protocol: deterministic facts, calibrated atomic answers, a composed verdict, a receipt, the on-chain record and the evaluator call. No runtime dependencies, no key inside.",
3
+ "version": "0.2.1",
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",
7
7
  "typesafe",
8
- "judge",
9
- "grader",
8
+ "typesafe-ai",
9
+ "system-one",
10
+ "llm-as-a-judge",
11
+ "llm-judge",
10
12
  "evaluator",
13
+ "grader",
11
14
  "erc-8183",
12
- "agentic-commerce",
15
+ "virtuals",
13
16
  "acp",
17
+ "agentic-commerce",
18
+ "ai-agents",
14
19
  "on-chain",
15
- "calibrated"
20
+ "attestation",
21
+ "base"
16
22
  ],
17
23
  "license": "MIT",
18
24
  "author": {
@@ -20,14 +26,13 @@
20
26
  "email": "yawning.monsoon@proton.me",
21
27
  "url": "https://github.com/taifoon-io"
22
28
  },
23
- "homepage": "https://github.com/taifoon-io/n8n-nodes-typesafe/tree/main/jev#readme",
29
+ "homepage": "https://github.com/taifoon-io/jev#readme",
24
30
  "repository": {
25
31
  "type": "git",
26
- "url": "git+https://github.com/taifoon-io/n8n-nodes-typesafe.git",
27
- "directory": "jev"
32
+ "url": "git+https://github.com/taifoon-io/jev.git"
28
33
  },
29
34
  "bugs": {
30
- "url": "https://github.com/taifoon-io/n8n-nodes-typesafe/issues"
35
+ "url": "https://github.com/taifoon-io/jev/issues"
31
36
  },
32
37
  "type": "module",
33
38
  "main": "./dist/index.js",
@@ -42,6 +47,8 @@
42
47
  "dist",
43
48
  "bin",
44
49
  "workflows",
50
+ "docs",
51
+ "examples",
45
52
  "README.md",
46
53
  "LICENSE"
47
54
  ],
@@ -68,6 +75,7 @@
68
75
  "vitest": "^4.1.8"
69
76
  },
70
77
  "bin": {
71
- "jev": "bin/jev.mjs"
78
+ "jev": "bin/jev.mjs",
79
+ "taifoon-jev": "bin/jev.mjs"
72
80
  }
73
81
  }
@@ -1,4 +1,4 @@
1
- # Jev workflows 0.1.0
1
+ # Jev workflows 0.2.1
2
2
 
3
3
  The n8n workflows that run Jev on n8n.taifoon.dev, exported with credential references replaced by placeholders (`{ "id": "", "name": "REPLACE: <credential type>" }`: pick your own credential of that type after import). Import with n8n → Workflows → Import from File, or `n8n import:workflow --input=<file>`. Every step carries its contract: `meta.taifoon.steps` names the canonical entity it takes and emits by `$id`; the schemas are in `schemas/` (the `$id`s are identifiers, the files are here).
4
4
 
@@ -50,7 +50,7 @@ One hire from offer to settlement. The seller’s reply is held with its digest;
50
50
 
51
51
  Grades Base jobs that were paid and delivered and that nobody ruled on (GET /v1/judge/ready): four per run, each through prepare → Jev → answers, then the canonical verdict, then GradeStampRegistry.stamp on the devnet, signed by the devnet signer node; the stamp transaction is appended to the verdict’s anchors.
52
52
 
53
- - source: `tfnjevgrader0001` on n8n.taifoon.dev · sha256 `06159f5ee2ea51644d1cca6cbdc79024fb565bee0720e622f97e52a28652c98d`
53
+ - source: `tfnjevgrader0001` on n8n.taifoon.dev · sha256 `762248c554dd0607362e20361ec3efe0fda58326d89c2f1c18c525ea48e92e35`
54
54
  - nodes: `n8n-nodes-taifoon`, `@taifoon/n8n-nodes-typesafe`, `n8n-nodes-taifoon-devnet-signer`; built-ins manualTrigger v1, code v2, if v2.2
55
55
  - credentials: `taifoonRelayerApi`, `taifoonTypeSafeApi`, `taifoonDevnetKeyApi`
56
56
 
@@ -91,7 +91,7 @@ A batch of subjects, each as its own prepare → Jev → answers (so every answe
91
91
 
92
92
  Takes one recorded decision (by id), maps it to the canonical verdict, and stamps it on the devnet GradeStampRegistry; emits the verdict with the stamp transaction in its anchors.
93
93
 
94
- - source: `tfnstamp00000001` on n8n.taifoon.dev · sha256 `4464558aa9593b2bf3f2cbb6b95a0c7d2ab83c549947f96195322624da7028e4`
94
+ - source: `tfnstamp00000001` on n8n.taifoon.dev · sha256 `516d9483e3d26e1891dacb8c4493d609340e3c6a3b200784c72864f9cb931ebc`
95
95
  - nodes: `n8n-nodes-taifoon`, `n8n-nodes-taifoon-devnet-signer`; built-ins manualTrigger v1, code v2
96
96
  - credentials: `taifoonDevnetKeyApi`
97
97
 
@@ -174,7 +174,7 @@
174
174
  {
175
175
  "parameters": {
176
176
  "mode": "runOnceForEachItem",
177
- "jsCode": "// ── canonical VERDICT (taifoon.verdict.v1) from a decision record — the same mapping as schemas/src/from/judge.ts\n// fromDecision, inlined because an n8n code node cannot import the schemas package. workflows/test.mjs runs this node\n// and compares its output with fromDecision field by field.\nconst SITE = 'https://www.taifoon.io';\nconst isHash = (s) => typeof s === 'string' && /^0x[0-9a-fA-F]{64}$/.test(s);\nconst chainAnchor = (chain_id, tx, what, x = {}) => ({ kind: 'chain', chain_id, tx: tx.toLowerCase(), block: x.block ?? null, ts: x.ts ?? null, log_index: x.log_index ?? null, contract: x.contract ? x.contract.toLowerCase() : null, what });\nfunction jobKeyOf(subject) {\n const ref = String(subject && subject.ref || ''), chain = Number(subject && subject.chainId) || 8453;\n if (/^hs_[0-9a-f]{24}$/.test(ref)) return 'hs:' + ref;\n if (/^([a-z][a-z0-9-]{0,31}):([1-9][0-9]{0,9}):([0-9]{1,78})$/.test(ref)) return ref;\n if (/^0x[0-9a-fA-F]{64}$/.test(ref)) return 'hook:' + chain + ':' + ref.toLowerCase();\n if (/^[0-9]{1,78}$/.test(ref)) return /^[0-9]{10,12}$/.test(ref) && BigInt(ref) >= 1000000001n ? 'memo-acp:8453:' + ref : 'erc8183:' + chain + ':' + ref;\n return null;\n}\nfunction verdictFromDecision(d, composed) {\n const rubric = /RUBRIC_v1 compose · rubric (0x[0-9a-f]{64})/.exec(String(d.note ?? ''));\n const anchors = [];\n if (d.anchor && isHash(d.anchor.tx)) anchors.push(chainAnchor(Number(d.anchor.chain), d.anchor.tx, `JevDecisionLog.record${d.anchor.index != null ? ` (decision index ${d.anchor.index})` : ''} · status ${d.anchor.status}`, { block: d.anchor.block ?? null, contract: d.anchor.contract ?? null }));\n anchors.push({ kind: 'offchain', store: 'coordd:decisions', key: String(d.id), digest: isHash(d.digest) ? d.digest.toLowerCase() : null, anchored_by: anchors[0] && anchors[0].kind === 'chain' ? anchors[0] : null, what: 'the decision record; digest = sha256(canonicalDecision)' });\n return {\n schema: 'taifoon.verdict.v1', decision_id: String(d.id), version: d.v, kind: d.kind,\n subject: { chain_id: Number(d.subject && d.subject.chainId), at: d.subject && d.subject.at ? String(d.subject.at).toLowerCase() : null, ref: String(d.subject && d.subject.ref), label: (d.subject && d.subject.label) ?? null },\n subject_id: String(d.subject_id).toLowerCase(), subject_job_key: jobKeyOf(d.subject), digest: String(d.digest).toLowerCase(),\n input_digest: isHash(d.input_digest) ? d.input_digest.toLowerCase() : null, answers_digest: isHash(d.answers_digest) ? d.answers_digest.toLowerCase() : null,\n answers: (d.answers || []).map((a) => ({ id: String(a.id), question: a.question ?? null, options: a.options ?? null, value: String(a.value), probabilities: a.probabilities ?? {}, confidence: Number(a.confidence) })),\n confidence_bps: d.confidence_bps, model: d.model ?? null, answers_source: d.answers_source ?? 'layer',\n rubric: rubric ? { version: 'RUBRIC_v1', hash: rubric[1] } : null,\n composed: composed ? { verdict: composed.verdict, auto: Boolean(composed.auto), forced: composed.forced ?? null, reasons: (composed.reasons || []).map(String), receipt_hash: isHash(composed.receiptHash) ? composed.receiptHash.toLowerCase() : null } : null,\n anchors, source: { kind: 'decision', url: d.uri ?? `${SITE}/v1/judge/decisions/${d.id}`, read_at: Math.floor(Date.now() / 1000) },\n };\n}\n// the GradeStampRegistry verdict code (1 delivered_as_specified · 2 delivered_with_defects · 3 not_delivered · 4 cannot_determine)\nconst STAMP_CODE = { delivered_as_specified: 1, delivered_with_defects: 2, not_delivered: 3, cannot_determine: 4 };\nfunction stampCodeOf(v) {\n if (v.composed) return v.composed.verdict === 'complete' ? 1 : v.composed.verdict === 'reject' ? 2 : 4;\n const ref = (v.answers || []).find((a) => STAMP_CODE[a.value]);\n return ref ? STAMP_CODE[ref.value] : 4;\n}\n// the stamp transaction(s) the devnet signer mined → appended to the verdict's anchors\nconst stampAnchors = (sent, chainId) => (sent || []).filter((s) => isHash(s.tx) && s.status === 'success').map((s) => chainAnchor(chainId, s.tx, 'GradeStampRegistry.stamp (' + (s.action || 'stamp') + ')', { block: s.block ?? null, contract: s.to ?? null }));\n\n// the recorded decision → the canonical VERDICT, composed from this run's receipt\nconst a = $json;\nif (!a.decision || !a.decision.id) throw new Error('judge answers returned no decision: ' + JSON.stringify(a).slice(0, 300));\nconst d = (await this.helpers.httpRequest({ method: 'GET', url: SITE + '/v1/judge/decisions/' + a.decision.id, json: true })).decision;\nconst v = verdictFromDecision(d, a.receipt);\nreturn { json: { ...v, stamp: { code: stampCodeOf(v), mode: 2 } } };"
177
+ "jsCode": "// ── canonical VERDICT (taifoon.verdict.v1) from a decision record — the same mapping as the Taifoon verdict schema\n// fromDecision, inlined because an n8n code node cannot import the schemas package. workflows/test.mjs runs this node\n// and compares its output with fromDecision field by field.\nconst SITE = 'https://www.taifoon.io';\nconst isHash = (s) => typeof s === 'string' && /^0x[0-9a-fA-F]{64}$/.test(s);\nconst chainAnchor = (chain_id, tx, what, x = {}) => ({ kind: 'chain', chain_id, tx: tx.toLowerCase(), block: x.block ?? null, ts: x.ts ?? null, log_index: x.log_index ?? null, contract: x.contract ? x.contract.toLowerCase() : null, what });\nfunction jobKeyOf(subject) {\n const ref = String(subject && subject.ref || ''), chain = Number(subject && subject.chainId) || 8453;\n if (/^hs_[0-9a-f]{24}$/.test(ref)) return 'hs:' + ref;\n if (/^([a-z][a-z0-9-]{0,31}):([1-9][0-9]{0,9}):([0-9]{1,78})$/.test(ref)) return ref;\n if (/^0x[0-9a-fA-F]{64}$/.test(ref)) return 'hook:' + chain + ':' + ref.toLowerCase();\n if (/^[0-9]{1,78}$/.test(ref)) return /^[0-9]{10,12}$/.test(ref) && BigInt(ref) >= 1000000001n ? 'memo-acp:8453:' + ref : 'erc8183:' + chain + ':' + ref;\n return null;\n}\nfunction verdictFromDecision(d, composed) {\n const rubric = /RUBRIC_v1 compose · rubric (0x[0-9a-f]{64})/.exec(String(d.note ?? ''));\n const anchors = [];\n if (d.anchor && isHash(d.anchor.tx)) anchors.push(chainAnchor(Number(d.anchor.chain), d.anchor.tx, `JevDecisionLog.record${d.anchor.index != null ? ` (decision index ${d.anchor.index})` : ''} · status ${d.anchor.status}`, { block: d.anchor.block ?? null, contract: d.anchor.contract ?? null }));\n anchors.push({ kind: 'offchain', store: 'coordd:decisions', key: String(d.id), digest: isHash(d.digest) ? d.digest.toLowerCase() : null, anchored_by: anchors[0] && anchors[0].kind === 'chain' ? anchors[0] : null, what: 'the decision record; digest = sha256(canonicalDecision)' });\n return {\n schema: 'taifoon.verdict.v1', decision_id: String(d.id), version: d.v, kind: d.kind,\n subject: { chain_id: Number(d.subject && d.subject.chainId), at: d.subject && d.subject.at ? String(d.subject.at).toLowerCase() : null, ref: String(d.subject && d.subject.ref), label: (d.subject && d.subject.label) ?? null },\n subject_id: String(d.subject_id).toLowerCase(), subject_job_key: jobKeyOf(d.subject), digest: String(d.digest).toLowerCase(),\n input_digest: isHash(d.input_digest) ? d.input_digest.toLowerCase() : null, answers_digest: isHash(d.answers_digest) ? d.answers_digest.toLowerCase() : null,\n answers: (d.answers || []).map((a) => ({ id: String(a.id), question: a.question ?? null, options: a.options ?? null, value: String(a.value), probabilities: a.probabilities ?? {}, confidence: Number(a.confidence) })),\n confidence_bps: d.confidence_bps, model: d.model ?? null, answers_source: d.answers_source ?? 'layer',\n rubric: rubric ? { version: 'RUBRIC_v1', hash: rubric[1] } : null,\n composed: composed ? { verdict: composed.verdict, auto: Boolean(composed.auto), forced: composed.forced ?? null, reasons: (composed.reasons || []).map(String), receipt_hash: isHash(composed.receiptHash) ? composed.receiptHash.toLowerCase() : null } : null,\n anchors, source: { kind: 'decision', url: d.uri ?? `${SITE}/v1/judge/decisions/${d.id}`, read_at: Math.floor(Date.now() / 1000) },\n };\n}\n// the GradeStampRegistry verdict code (1 delivered_as_specified · 2 delivered_with_defects · 3 not_delivered · 4 cannot_determine)\nconst STAMP_CODE = { delivered_as_specified: 1, delivered_with_defects: 2, not_delivered: 3, cannot_determine: 4 };\nfunction stampCodeOf(v) {\n if (v.composed) return v.composed.verdict === 'complete' ? 1 : v.composed.verdict === 'reject' ? 2 : 4;\n const ref = (v.answers || []).find((a) => STAMP_CODE[a.value]);\n return ref ? STAMP_CODE[ref.value] : 4;\n}\n// the stamp transaction(s) the devnet signer mined → appended to the verdict's anchors\nconst stampAnchors = (sent, chainId) => (sent || []).filter((s) => isHash(s.tx) && s.status === 'success').map((s) => chainAnchor(chainId, s.tx, 'GradeStampRegistry.stamp (' + (s.action || 'stamp') + ')', { block: s.block ?? null, contract: s.to ?? null }));\n\n// the recorded decision → the canonical VERDICT, composed from this run's receipt\nconst a = $json;\nif (!a.decision || !a.decision.id) throw new Error('judge answers returned no decision: ' + JSON.stringify(a).slice(0, 300));\nconst d = (await this.helpers.httpRequest({ method: 'GET', url: SITE + '/v1/judge/decisions/' + a.decision.id, json: true })).decision;\nconst v = verdictFromDecision(d, a.receipt);\nreturn { json: { ...v, stamp: { code: stampCodeOf(v), mode: 2 } } };"
178
178
  },
179
179
  "id": "dd2f2612-43ba-4e52-aa54-bf147e4dd561",
180
180
  "name": "Verdict",
@@ -236,7 +236,7 @@
236
236
  {
237
237
  "parameters": {
238
238
  "mode": "runOnceForEachItem",
239
- "jsCode": "// ── canonical VERDICT (taifoon.verdict.v1) from a decision record — the same mapping as schemas/src/from/judge.ts\n// fromDecision, inlined because an n8n code node cannot import the schemas package. workflows/test.mjs runs this node\n// and compares its output with fromDecision field by field.\nconst SITE = 'https://www.taifoon.io';\nconst isHash = (s) => typeof s === 'string' && /^0x[0-9a-fA-F]{64}$/.test(s);\nconst chainAnchor = (chain_id, tx, what, x = {}) => ({ kind: 'chain', chain_id, tx: tx.toLowerCase(), block: x.block ?? null, ts: x.ts ?? null, log_index: x.log_index ?? null, contract: x.contract ? x.contract.toLowerCase() : null, what });\nfunction jobKeyOf(subject) {\n const ref = String(subject && subject.ref || ''), chain = Number(subject && subject.chainId) || 8453;\n if (/^hs_[0-9a-f]{24}$/.test(ref)) return 'hs:' + ref;\n if (/^([a-z][a-z0-9-]{0,31}):([1-9][0-9]{0,9}):([0-9]{1,78})$/.test(ref)) return ref;\n if (/^0x[0-9a-fA-F]{64}$/.test(ref)) return 'hook:' + chain + ':' + ref.toLowerCase();\n if (/^[0-9]{1,78}$/.test(ref)) return /^[0-9]{10,12}$/.test(ref) && BigInt(ref) >= 1000000001n ? 'memo-acp:8453:' + ref : 'erc8183:' + chain + ':' + ref;\n return null;\n}\nfunction verdictFromDecision(d, composed) {\n const rubric = /RUBRIC_v1 compose · rubric (0x[0-9a-f]{64})/.exec(String(d.note ?? ''));\n const anchors = [];\n if (d.anchor && isHash(d.anchor.tx)) anchors.push(chainAnchor(Number(d.anchor.chain), d.anchor.tx, `JevDecisionLog.record${d.anchor.index != null ? ` (decision index ${d.anchor.index})` : ''} · status ${d.anchor.status}`, { block: d.anchor.block ?? null, contract: d.anchor.contract ?? null }));\n anchors.push({ kind: 'offchain', store: 'coordd:decisions', key: String(d.id), digest: isHash(d.digest) ? d.digest.toLowerCase() : null, anchored_by: anchors[0] && anchors[0].kind === 'chain' ? anchors[0] : null, what: 'the decision record; digest = sha256(canonicalDecision)' });\n return {\n schema: 'taifoon.verdict.v1', decision_id: String(d.id), version: d.v, kind: d.kind,\n subject: { chain_id: Number(d.subject && d.subject.chainId), at: d.subject && d.subject.at ? String(d.subject.at).toLowerCase() : null, ref: String(d.subject && d.subject.ref), label: (d.subject && d.subject.label) ?? null },\n subject_id: String(d.subject_id).toLowerCase(), subject_job_key: jobKeyOf(d.subject), digest: String(d.digest).toLowerCase(),\n input_digest: isHash(d.input_digest) ? d.input_digest.toLowerCase() : null, answers_digest: isHash(d.answers_digest) ? d.answers_digest.toLowerCase() : null,\n answers: (d.answers || []).map((a) => ({ id: String(a.id), question: a.question ?? null, options: a.options ?? null, value: String(a.value), probabilities: a.probabilities ?? {}, confidence: Number(a.confidence) })),\n confidence_bps: d.confidence_bps, model: d.model ?? null, answers_source: d.answers_source ?? 'layer',\n rubric: rubric ? { version: 'RUBRIC_v1', hash: rubric[1] } : null,\n composed: composed ? { verdict: composed.verdict, auto: Boolean(composed.auto), forced: composed.forced ?? null, reasons: (composed.reasons || []).map(String), receipt_hash: isHash(composed.receiptHash) ? composed.receiptHash.toLowerCase() : null } : null,\n anchors, source: { kind: 'decision', url: d.uri ?? `${SITE}/v1/judge/decisions/${d.id}`, read_at: Math.floor(Date.now() / 1000) },\n };\n}\n// the GradeStampRegistry verdict code (1 delivered_as_specified · 2 delivered_with_defects · 3 not_delivered · 4 cannot_determine)\nconst STAMP_CODE = { delivered_as_specified: 1, delivered_with_defects: 2, not_delivered: 3, cannot_determine: 4 };\nfunction stampCodeOf(v) {\n if (v.composed) return v.composed.verdict === 'complete' ? 1 : v.composed.verdict === 'reject' ? 2 : 4;\n const ref = (v.answers || []).find((a) => STAMP_CODE[a.value]);\n return ref ? STAMP_CODE[ref.value] : 4;\n}\n// the stamp transaction(s) the devnet signer mined → appended to the verdict's anchors\nconst stampAnchors = (sent, chainId) => (sent || []).filter((s) => isHash(s.tx) && s.status === 'success').map((s) => chainAnchor(chainId, s.tx, 'GradeStampRegistry.stamp (' + (s.action || 'stamp') + ')', { block: s.block ?? null, contract: s.to ?? null }));\n\n// the same VERDICT with the GradeStampRegistry stamp transaction appended to its anchors\nconst v = { ...$('Verdict').item.json };\ndelete v.stamp;\nreturn { json: { ...v, anchors: [...v.anchors, ...stampAnchors($json.sent, 36927)] } };"
239
+ "jsCode": "// ── canonical VERDICT (taifoon.verdict.v1) from a decision record — the same mapping as the Taifoon verdict schema\n// fromDecision, inlined because an n8n code node cannot import the schemas package. workflows/test.mjs runs this node\n// and compares its output with fromDecision field by field.\nconst SITE = 'https://www.taifoon.io';\nconst isHash = (s) => typeof s === 'string' && /^0x[0-9a-fA-F]{64}$/.test(s);\nconst chainAnchor = (chain_id, tx, what, x = {}) => ({ kind: 'chain', chain_id, tx: tx.toLowerCase(), block: x.block ?? null, ts: x.ts ?? null, log_index: x.log_index ?? null, contract: x.contract ? x.contract.toLowerCase() : null, what });\nfunction jobKeyOf(subject) {\n const ref = String(subject && subject.ref || ''), chain = Number(subject && subject.chainId) || 8453;\n if (/^hs_[0-9a-f]{24}$/.test(ref)) return 'hs:' + ref;\n if (/^([a-z][a-z0-9-]{0,31}):([1-9][0-9]{0,9}):([0-9]{1,78})$/.test(ref)) return ref;\n if (/^0x[0-9a-fA-F]{64}$/.test(ref)) return 'hook:' + chain + ':' + ref.toLowerCase();\n if (/^[0-9]{1,78}$/.test(ref)) return /^[0-9]{10,12}$/.test(ref) && BigInt(ref) >= 1000000001n ? 'memo-acp:8453:' + ref : 'erc8183:' + chain + ':' + ref;\n return null;\n}\nfunction verdictFromDecision(d, composed) {\n const rubric = /RUBRIC_v1 compose · rubric (0x[0-9a-f]{64})/.exec(String(d.note ?? ''));\n const anchors = [];\n if (d.anchor && isHash(d.anchor.tx)) anchors.push(chainAnchor(Number(d.anchor.chain), d.anchor.tx, `JevDecisionLog.record${d.anchor.index != null ? ` (decision index ${d.anchor.index})` : ''} · status ${d.anchor.status}`, { block: d.anchor.block ?? null, contract: d.anchor.contract ?? null }));\n anchors.push({ kind: 'offchain', store: 'coordd:decisions', key: String(d.id), digest: isHash(d.digest) ? d.digest.toLowerCase() : null, anchored_by: anchors[0] && anchors[0].kind === 'chain' ? anchors[0] : null, what: 'the decision record; digest = sha256(canonicalDecision)' });\n return {\n schema: 'taifoon.verdict.v1', decision_id: String(d.id), version: d.v, kind: d.kind,\n subject: { chain_id: Number(d.subject && d.subject.chainId), at: d.subject && d.subject.at ? String(d.subject.at).toLowerCase() : null, ref: String(d.subject && d.subject.ref), label: (d.subject && d.subject.label) ?? null },\n subject_id: String(d.subject_id).toLowerCase(), subject_job_key: jobKeyOf(d.subject), digest: String(d.digest).toLowerCase(),\n input_digest: isHash(d.input_digest) ? d.input_digest.toLowerCase() : null, answers_digest: isHash(d.answers_digest) ? d.answers_digest.toLowerCase() : null,\n answers: (d.answers || []).map((a) => ({ id: String(a.id), question: a.question ?? null, options: a.options ?? null, value: String(a.value), probabilities: a.probabilities ?? {}, confidence: Number(a.confidence) })),\n confidence_bps: d.confidence_bps, model: d.model ?? null, answers_source: d.answers_source ?? 'layer',\n rubric: rubric ? { version: 'RUBRIC_v1', hash: rubric[1] } : null,\n composed: composed ? { verdict: composed.verdict, auto: Boolean(composed.auto), forced: composed.forced ?? null, reasons: (composed.reasons || []).map(String), receipt_hash: isHash(composed.receiptHash) ? composed.receiptHash.toLowerCase() : null } : null,\n anchors, source: { kind: 'decision', url: d.uri ?? `${SITE}/v1/judge/decisions/${d.id}`, read_at: Math.floor(Date.now() / 1000) },\n };\n}\n// the GradeStampRegistry verdict code (1 delivered_as_specified · 2 delivered_with_defects · 3 not_delivered · 4 cannot_determine)\nconst STAMP_CODE = { delivered_as_specified: 1, delivered_with_defects: 2, not_delivered: 3, cannot_determine: 4 };\nfunction stampCodeOf(v) {\n if (v.composed) return v.composed.verdict === 'complete' ? 1 : v.composed.verdict === 'reject' ? 2 : 4;\n const ref = (v.answers || []).find((a) => STAMP_CODE[a.value]);\n return ref ? STAMP_CODE[ref.value] : 4;\n}\n// the stamp transaction(s) the devnet signer mined → appended to the verdict's anchors\nconst stampAnchors = (sent, chainId) => (sent || []).filter((s) => isHash(s.tx) && s.status === 'success').map((s) => chainAnchor(chainId, s.tx, 'GradeStampRegistry.stamp (' + (s.action || 'stamp') + ')', { block: s.block ?? null, contract: s.to ?? null }));\n\n// the same VERDICT with the GradeStampRegistry stamp transaction appended to its anchors\nconst v = { ...$('Verdict').item.json };\ndelete v.stamp;\nreturn { json: { ...v, anchors: [...v.anchors, ...stampAnchors($json.sent, 36927)] } };"
240
240
  },
241
241
  "id": "bb65ea98-4d1f-4996-8498-b6a0a6c6c595",
242
242
  "name": "Verdict + stamp",
@@ -1,6 +1,6 @@
1
1
  {
2
- "bundle": "jev-workflows-0.1.0",
3
- "package": "@taifoon/jev@0.1.0",
2
+ "bundle": "jev-workflows-0.2.1",
3
+ "package": "@taifoon/jev@0.2.1",
4
4
  "workflows": [
5
5
  {
6
6
  "file": "hire-judge-settle.json",
@@ -29,7 +29,7 @@
29
29
  "file": "jev-grader.json",
30
30
  "source_id": "tfnjevgrader0001",
31
31
  "name": "Taifoon — Jev on-chain grader (grade → stamp → devnet)",
32
- "sha256": "06159f5ee2ea51644d1cca6cbdc79024fb565bee0720e622f97e52a28652c98d",
32
+ "sha256": "762248c554dd0607362e20361ec3efe0fda58326d89c2f1c18c525ea48e92e35",
33
33
  "nodes": 11,
34
34
  "packages": [
35
35
  "n8n-nodes-taifoon",
@@ -72,7 +72,7 @@
72
72
  "file": "stamp-grade.json",
73
73
  "source_id": "tfnstamp00000001",
74
74
  "name": "Taifoon — Stamp Grade (a recorded verdict → GradeStampRegistry on the devnet)",
75
- "sha256": "4464558aa9593b2bf3f2cbb6b95a0c7d2ab83c549947f96195322624da7028e4",
75
+ "sha256": "516d9483e3d26e1891dacb8c4493d609340e3c6a3b200784c72864f9cb931ebc",
76
76
  "nodes": 5,
77
77
  "packages": [
78
78
  "n8n-nodes-taifoon",
@@ -91,42 +91,42 @@
91
91
  {
92
92
  "file": "schemas/answers.json",
93
93
  "$id": "https://www.taifoon.io/schemas/coordination/v1/answers.json",
94
- "sha256": "3b0aeda27c39da1bcdeba73fc787e43c30403b427cde5545211bbd0cb0488a98"
94
+ "sha256": "16dcdee34b631c22fc97a06bc79ac49045209b5a729fbcae851c7816ff5fd208"
95
95
  },
96
96
  {
97
97
  "file": "schemas/common.json",
98
98
  "$id": "https://www.taifoon.io/schemas/coordination/v1/common.json",
99
- "sha256": "f878716e57b18f054cc0bed82a84d0c276e86ef03c00aff920a5ee9c9271867c"
99
+ "sha256": "d924619d04d607d1a07fd721c50ba61aa73523ffaac63b4459472d55c35e0da6"
100
100
  },
101
101
  {
102
102
  "file": "schemas/delivery.json",
103
103
  "$id": "https://www.taifoon.io/schemas/coordination/v1/delivery.json",
104
- "sha256": "9c8adcc0e4c525dc750946de5c51cae51b0f513b85e760aa86e3254b8fde7e37"
104
+ "sha256": "21a24f9436e2e8bbe39d8285feb29777a4afab75c053d4fe1fb7d43996b36da0"
105
105
  },
106
106
  {
107
107
  "file": "schemas/facts.json",
108
108
  "$id": "https://www.taifoon.io/schemas/coordination/v1/facts.json",
109
- "sha256": "d84e0fcbdf661c69dc6ee09a45603236749cbabae16107005a8b70d51d2401f1"
109
+ "sha256": "18c762ec1b35a819f302d73da293fd6c7320cb63bbbceb3a1d63143b1919e89f"
110
110
  },
111
111
  {
112
112
  "file": "schemas/hire.json",
113
113
  "$id": "https://www.taifoon.io/schemas/coordination/v1/hire.json",
114
- "sha256": "3951cd490e0e72a5ecd7efe8ba6de71a829011660f57147e0b17ffe5f0bb1143"
114
+ "sha256": "a9cccbbce8f1b4f50c31769f7af63fa9fc086e452d537a1e78612ba39448fa18"
115
115
  },
116
116
  {
117
117
  "file": "schemas/job.json",
118
118
  "$id": "https://www.taifoon.io/schemas/coordination/v1/job.json",
119
- "sha256": "12deb4119582da6e23229484d53e3fbd370becee6ef9a0bd31f1bec071701397"
119
+ "sha256": "c9fd5a4bcad766758b17dded08b877c047eaf51c318022bfa72aac45828ee357"
120
120
  },
121
121
  {
122
122
  "file": "schemas/settlement.json",
123
123
  "$id": "https://www.taifoon.io/schemas/coordination/v1/settlement.json",
124
- "sha256": "6063c33c2739a416a01af90e0b38c63218437b955feb8e457b067f4351faa3cc"
124
+ "sha256": "50e7e12c6f4b076d6f2df36fa2aedac09130d274bac223bc2aa0ea8f768ba233"
125
125
  },
126
126
  {
127
127
  "file": "schemas/verdict.json",
128
128
  "$id": "https://www.taifoon.io/schemas/coordination/v1/verdict.json",
129
- "sha256": "d8f5aee5a2e9c5146095c4061e3e463a232fda77f9031ba336959b70c906bd09"
129
+ "sha256": "4d80156faa135f28f5800f749a4f7ad8f22b72c422807a5254447032208b2942"
130
130
  }
131
131
  ],
132
132
  "packages": {
@@ -8,29 +8,24 @@
8
8
  "schema": {
9
9
  "const": "taifoon.answers.v1",
10
10
  "x-stage": "L",
11
- "x-source": "this contract",
12
11
  "description": "which canonical entity this record is; fixed per schema"
13
12
  },
14
13
  "answers_digest": {
15
14
  "$ref": "common.json#/$defs/bytes32",
16
- "x-stage": "S5",
17
- "x-source": "sha256(canonicalJson(record)) — JevAnswerLog answersDigest"
15
+ "x-stage": "S5"
18
16
  },
19
17
  "v": {
20
18
  "const": "jev.answer.v1",
21
- "x-stage": "S5",
22
- "x-source": "record version"
19
+ "x-stage": "S5"
23
20
  },
24
21
  "use_case": {
25
22
  "type": "string",
26
23
  "pattern": "^[a-z0-9][a-z0-9._/-]{0,63}$",
27
- "x-stage": "S5",
28
- "x-source": "ref · grade · compose · compose.answers · pad · suggest · assemble · study · registry.ref · runner.study · n8n.batch"
24
+ "x-stage": "S5"
29
25
  },
30
26
  "subject": {
31
27
  "$ref": "common.json#/$defs/bytes32",
32
- "x-stage": "S5",
33
- "x-source": "refBytes32(subject ref): a bytes32 passes through, anything else is keccak’d"
28
+ "x-stage": "S5"
34
29
  },
35
30
  "input_digest": {
36
31
  "anyOf": [
@@ -41,8 +36,7 @@
41
36
  "type": "null"
42
37
  }
43
38
  ],
44
- "x-stage": "S5",
45
- "x-source": "sha256 of exactly what Jev read"
39
+ "x-stage": "S5"
46
40
  },
47
41
  "questions": {
48
42
  "type": "array",
@@ -93,8 +87,7 @@
93
87
  ],
94
88
  "additionalProperties": false
95
89
  },
96
- "x-stage": "S5",
97
- "x-source": "the questions as asked"
90
+ "x-stage": "S5"
98
91
  },
99
92
  "answers": {
100
93
  "type": "array",
@@ -136,32 +129,28 @@
136
129
  "additionalProperties": false
137
130
  },
138
131
  "minItems": 1,
139
- "x-stage": "S5",
140
- "x-source": "value + the WHOLE distribution + confidence per question"
132
+ "x-stage": "S5"
141
133
  },
142
134
  "model": {
143
135
  "type": [
144
136
  "string",
145
137
  "null"
146
138
  ],
147
- "x-stage": "S5",
148
- "x-source": "model reported (jev-1.13.0)"
139
+ "x-stage": "S5"
149
140
  },
150
141
  "upstream_model": {
151
142
  "type": [
152
143
  "string",
153
144
  "null"
154
145
  ],
155
- "x-stage": "S5",
156
- "x-source": "upstream model"
146
+ "x-stage": "S5"
157
147
  },
158
148
  "latency_ms": {
159
149
  "type": [
160
150
  "integer",
161
151
  "null"
162
152
  ],
163
- "x-stage": "S5",
164
- "x-source": "model latency"
153
+ "x-stage": "S5"
165
154
  },
166
155
  "credential_path": {
167
156
  "enum": [
@@ -170,8 +159,7 @@
170
159
  "caller-credential",
171
160
  "registry"
172
161
  ],
173
- "x-stage": "S5",
174
- "x-source": "whose credential paid"
162
+ "x-stage": "S5"
175
163
  },
176
164
  "caller": {
177
165
  "enum": [
@@ -182,21 +170,18 @@
182
170
  "moonbeam",
183
171
  "runner"
184
172
  ],
185
- "x-stage": "S5",
186
- "x-source": "who asked"
173
+ "x-stage": "S5"
187
174
  },
188
175
  "at": {
189
176
  "$ref": "common.json#/$defs/unix_ms",
190
- "x-stage": "S5",
191
- "x-source": "ms since epoch"
177
+ "x-stage": "S5"
192
178
  },
193
179
  "decision_id": {
194
180
  "type": [
195
181
  "string",
196
182
  "null"
197
183
  ],
198
- "x-stage": "S5",
199
- "x-source": "the decision.v2 record these answers belong to"
184
+ "x-stage": "S5"
200
185
  },
201
186
  "decision_digest": {
202
187
  "anyOf": [
@@ -207,21 +192,18 @@
207
192
  "type": "null"
208
193
  }
209
194
  ],
210
- "x-stage": "S5",
211
- "x-source": "that decision’s digest (JevAnswerLog decisionDigest)"
195
+ "x-stage": "S5"
212
196
  },
213
197
  "anchors": {
214
198
  "type": "array",
215
199
  "items": {
216
200
  "$ref": "common.json#/$defs/anchor"
217
201
  },
218
- "x-stage": "S6",
219
- "x-source": "JevAnswerLog.record transaction (devnet 36927 0xD8c1…a8e3); coordd jevans:<digest>"
202
+ "x-stage": "S6"
220
203
  },
221
204
  "source": {
222
205
  "$ref": "common.json#/$defs/source",
223
- "x-stage": "L",
224
- "x-source": "the mapper"
206
+ "x-stage": "L"
225
207
  }
226
208
  },
227
209
  "required": [
@@ -367,7 +367,7 @@
367
367
  "evidence-pack",
368
368
  "job-classes"
369
369
  ],
370
- "description": "which real source this record was mapped from (schemas/src/from/)"
370
+ "description": "which real source this record was mapped from"
371
371
  },
372
372
  "url": {
373
373
  "type": [