@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
@@ -8,7 +8,6 @@
8
8
  "schema": {
9
9
  "const": "taifoon.settlement.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
  "job": {
@@ -20,21 +19,18 @@
20
19
  "type": "null"
21
20
  }
22
21
  ],
23
- "x-stage": "S6",
24
- "x-source": "settle:<jobId>; null when a runner row names no job id"
22
+ "x-stage": "S6"
25
23
  },
26
24
  "chain_id": {
27
25
  "$ref": "common.json#/$defs/chain_id",
28
- "x-stage": "S6",
29
- "x-source": "settlement chain"
26
+ "x-stage": "S6"
30
27
  },
31
28
  "tenant": {
32
29
  "type": [
33
30
  "string",
34
31
  "null"
35
32
  ],
36
- "x-stage": "S6",
37
- "x-source": "moonbeam | null (layer)"
33
+ "x-stage": "S6"
38
34
  },
39
35
  "kind": {
40
36
  "anyOf": [
@@ -49,8 +45,7 @@
49
45
  "type": "null"
50
46
  }
51
47
  ],
52
- "x-stage": "S6",
53
- "x-source": "settle.ts kind (runner rows)"
48
+ "x-stage": "S6"
54
49
  },
55
50
  "buyer": {
56
51
  "anyOf": [
@@ -61,8 +56,7 @@
61
56
  "type": "null"
62
57
  }
63
58
  ],
64
- "x-stage": "S6",
65
- "x-source": "hook buyer"
59
+ "x-stage": "S6"
66
60
  },
67
61
  "seller": {
68
62
  "anyOf": [
@@ -73,8 +67,7 @@
73
67
  "type": "null"
74
68
  }
75
69
  ],
76
- "x-stage": "S6",
77
- "x-source": "hook seller"
70
+ "x-stage": "S6"
78
71
  },
79
72
  "price": {
80
73
  "anyOf": [
@@ -85,8 +78,7 @@
85
78
  "type": "null"
86
79
  }
87
80
  ],
88
- "x-stage": "S6",
89
- "x-source": "price in the token’s smallest unit"
81
+ "x-stage": "S6"
90
82
  },
91
83
  "token": {
92
84
  "anyOf": [
@@ -97,8 +89,7 @@
97
89
  "type": "null"
98
90
  }
99
91
  ],
100
- "x-stage": "S6",
101
- "x-source": "job token"
92
+ "x-stage": "S6"
102
93
  },
103
94
  "verdict": {
104
95
  "anyOf": [
@@ -114,8 +105,7 @@
114
105
  "type": "null"
115
106
  }
116
107
  ],
117
- "x-stage": "S6",
118
- "x-source": "settle-records verdictOfState"
108
+ "x-stage": "S6"
119
109
  },
120
110
  "state": {
121
111
  "anyOf": [
@@ -126,16 +116,14 @@
126
116
  "type": "null"
127
117
  }
128
118
  ],
129
- "x-stage": "L",
130
- "x-source": "the terminal machine state the verdict word names"
119
+ "x-stage": "L"
131
120
  },
132
121
  "decision_id": {
133
122
  "type": [
134
123
  "string",
135
124
  "null"
136
125
  ],
137
- "x-stage": "S5",
138
- "x-source": "the decision that drove it, when recorded"
126
+ "x-stage": "S5"
139
127
  },
140
128
  "txs": {
141
129
  "type": "array",
@@ -162,19 +150,18 @@
162
150
  ],
163
151
  "additionalProperties": false
164
152
  },
165
- "x-stage": "S6",
166
- "x-source": "every transaction, unioned by hash"
153
+ "x-stage": "S6"
167
154
  },
168
155
  "writer": {
169
156
  "enum": [
170
157
  "runner",
171
158
  "relayer",
172
159
  "evaluator-leg",
160
+ "keeper",
173
161
  "site",
174
162
  "n8n"
175
163
  ],
176
- "x-stage": "L",
177
- "x-source": "which write path recorded it (n8n = a workflow that signed its own legs, e.g. the devnet signer)"
164
+ "x-stage": "L"
178
165
  },
179
166
  "at": {
180
167
  "anyOf": [
@@ -185,21 +172,18 @@
185
172
  "type": "null"
186
173
  }
187
174
  ],
188
- "x-stage": "L",
189
- "x-source": "first write"
175
+ "x-stage": "L"
190
176
  },
191
177
  "anchors": {
192
178
  "type": "array",
193
179
  "items": {
194
180
  "$ref": "common.json#/$defs/anchor"
195
181
  },
196
- "x-stage": "S6",
197
- "x-source": "the settlement transactions"
182
+ "x-stage": "S6"
198
183
  },
199
184
  "source": {
200
185
  "$ref": "common.json#/$defs/source",
201
- "x-stage": "L",
202
- "x-source": "the mapper"
186
+ "x-stage": "L"
203
187
  }
204
188
  },
205
189
  "required": [
@@ -8,22 +8,19 @@
8
8
  "schema": {
9
9
  "const": "taifoon.verdict.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
  "decision_id": {
15
14
  "type": "string",
16
15
  "pattern": "^decision-[0-9]+-[0-9a-f]{10}$",
17
- "x-stage": "S5",
18
- "x-source": "decision-<ms>-<digest[2..12]>"
16
+ "x-stage": "S5"
19
17
  },
20
18
  "version": {
21
19
  "enum": [
22
20
  "decision.v1",
23
21
  "decision.v2"
24
22
  ],
25
- "x-stage": "S5",
26
- "x-source": "v2 pins input_digest inside the digest"
23
+ "x-stage": "S5"
27
24
  },
28
25
  "kind": {
29
26
  "enum": [
@@ -34,8 +31,7 @@
34
31
  "grade",
35
32
  "study"
36
33
  ],
37
- "x-stage": "S5",
38
- "x-source": "where the layer asked the judge"
34
+ "x-stage": "S5"
39
35
  },
40
36
  "subject": {
41
37
  "type": "object",
@@ -70,13 +66,11 @@
70
66
  "label"
71
67
  ],
72
68
  "additionalProperties": false,
73
- "x-stage": "S5",
74
- "x-source": "DecisionSubject (chainId, at, ref, label)"
69
+ "x-stage": "S5"
75
70
  },
76
71
  "subject_id": {
77
72
  "$ref": "common.json#/$defs/bytes32",
78
- "x-stage": "S5",
79
- "x-source": "keccak256(abi.encode(\"taifoon.decision.subject.v1\", chainId, at, refBytes32(ref))) — JevDecisionLog.subjectOf"
73
+ "x-stage": "S5"
80
74
  },
81
75
  "subject_job_key": {
82
76
  "anyOf": [
@@ -87,13 +81,11 @@
87
81
  "type": "null"
88
82
  }
89
83
  ],
90
- "x-stage": "L",
91
- "x-source": "the job the subject ref names, when it names one (hs_… → hs:…, bytes32 on a hook chain → hook:<chain>:…)"
84
+ "x-stage": "L"
92
85
  },
93
86
  "digest": {
94
87
  "$ref": "common.json#/$defs/bytes32",
95
- "x-stage": "S5",
96
- "x-source": "sha256(canonicalDecision) — the 32 bytes anchored and posted by the evaluator leg"
88
+ "x-stage": "S5"
97
89
  },
98
90
  "input_digest": {
99
91
  "anyOf": [
@@ -104,8 +96,7 @@
104
96
  "type": "null"
105
97
  }
106
98
  ],
107
- "x-stage": "S5",
108
- "x-source": "sha256 of the text Jev read (inside the digest from v2)"
99
+ "x-stage": "S5"
109
100
  },
110
101
  "answers_digest": {
111
102
  "anyOf": [
@@ -116,8 +107,7 @@
116
107
  "type": "null"
117
108
  }
118
109
  ],
119
- "x-stage": "S5",
120
- "x-source": "the jev.answer.v1 record of these answers"
110
+ "x-stage": "S5"
121
111
  },
122
112
  "answers": {
123
113
  "type": "array",
@@ -175,31 +165,27 @@
175
165
  ],
176
166
  "additionalProperties": false
177
167
  },
178
- "x-stage": "S5",
179
- "x-source": "DecisionAnswer[]"
168
+ "x-stage": "S5"
180
169
  },
181
170
  "confidence_bps": {
182
171
  "type": "integer",
183
172
  "minimum": 0,
184
173
  "maximum": 10000,
185
- "x-stage": "S5",
186
- "x-source": "LOWEST answer confidence × 10,000"
174
+ "x-stage": "S5"
187
175
  },
188
176
  "model": {
189
177
  "type": [
190
178
  "string",
191
179
  "null"
192
180
  ],
193
- "x-stage": "S5",
194
- "x-source": "model"
181
+ "x-stage": "S5"
195
182
  },
196
183
  "answers_source": {
197
184
  "enum": [
198
185
  "layer",
199
186
  "caller-credential"
200
187
  ],
201
- "x-stage": "S5",
202
- "x-source": "who fetched the answers (compose two-step: caller-credential)"
188
+ "x-stage": "S5"
203
189
  },
204
190
  "rubric": {
205
191
  "type": [
@@ -219,8 +205,7 @@
219
205
  "hash"
220
206
  ],
221
207
  "additionalProperties": false,
222
- "x-stage": "S5",
223
- "x-source": "RUBRIC_v1 + sha256(RUBRIC_V1) when the decision is a compose (note \"RUBRIC_v1 compose · rubric 0x…\")"
208
+ "x-stage": "S5"
224
209
  },
225
210
  "composed": {
226
211
  "type": [
@@ -273,21 +258,18 @@
273
258
  "receipt_hash"
274
259
  ],
275
260
  "additionalProperties": false,
276
- "x-stage": "S5",
277
- "x-source": "judge-compose receipt() when the source carries it; null when only the decision is at hand (recompose with judge-compose.compose(facts, answers))"
261
+ "x-stage": "S5"
278
262
  },
279
263
  "anchors": {
280
264
  "type": "array",
281
265
  "items": {
282
266
  "$ref": "common.json#/$defs/anchor"
283
267
  },
284
- "x-stage": "S6",
285
- "x-source": "JevDecisionLog.record transaction (devnet 36927 0x1D62…9A05); coordd decision-<id>"
268
+ "x-stage": "S6"
286
269
  },
287
270
  "source": {
288
271
  "$ref": "common.json#/$defs/source",
289
- "x-stage": "L",
290
- "x-source": "the mapper"
272
+ "x-stage": "L"
291
273
  }
292
274
  },
293
275
  "required": [
@@ -0,0 +1,175 @@
1
+ {
2
+ "name": "Taifoon — Stamp Grade (a recorded verdict → GradeStampRegistry on the devnet)",
3
+ "nodes": [
4
+ {
5
+ "parameters": {},
6
+ "id": "a1",
7
+ "name": "Start",
8
+ "type": "n8n-nodes-base.manualTrigger",
9
+ "typeVersion": 1,
10
+ "position": [
11
+ 0,
12
+ 0
13
+ ],
14
+ "notes": "in: — → out: — · start (pass { decision_id } to stamp another verdict)",
15
+ "notesInFlow": true
16
+ },
17
+ {
18
+ "parameters": {
19
+ "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 verdict to stamp: a recorded decision (pass { decision_id } on the trigger; the default is job 81067's REF decision)\nconst t = $input.first().json || {};\nconst id = t.decision_id || 'decision-1790239759511-7fc88c5799';\nconst d = (await this.helpers.httpRequest({ method: 'GET', url: SITE + '/v1/judge/decisions/' + id, json: true })).decision;\nconst v = verdictFromDecision(d, null);\nreturn [{ json: { ...v, stamp: { code: stampCodeOf(v), mode: 2 } } }];"
20
+ },
21
+ "id": "e00116ba-ac40-4cbe-9c4c-bddf65b3fc6d",
22
+ "name": "Verdict",
23
+ "type": "n8n-nodes-base.code",
24
+ "typeVersion": 2,
25
+ "position": [
26
+ 420,
27
+ 300
28
+ ],
29
+ "notes": "in: — → out: verdict.json · GET /v1/judge/decisions/:id → the canonical VERDICT (fromDecision) and its stamp code",
30
+ "notesInFlow": true
31
+ },
32
+ {
33
+ "parameters": {
34
+ "resource": "assurance",
35
+ "operation": "stampGrade",
36
+ "chainId": 36927,
37
+ "stampSubject": "={{ $json.subject_id }}",
38
+ "stampDigest": "={{ $json.digest }}",
39
+ "stampVerdict": "={{ String($json.stamp.code) }}",
40
+ "stampMode": "={{ String($json.stamp.mode) }}",
41
+ "stampModel": "={{ $json.model || 'jev' }}",
42
+ "stampUri": "={{ $json.source.url }}"
43
+ },
44
+ "id": "b2",
45
+ "name": "Stamp Grade",
46
+ "type": "CUSTOM.taifoon",
47
+ "typeVersion": 1,
48
+ "position": [
49
+ 640,
50
+ 300
51
+ ],
52
+ "notes": "in: verdict.json → out: verdict.json · POST /v1/assurance/call stampGrade: the unsigned GradeStampRegistry.stamp(subject_id, digest, verdict code, mode)",
53
+ "notesInFlow": true
54
+ },
55
+ {
56
+ "parameters": {
57
+ "callsField": "calls",
58
+ "sendRequires": false,
59
+ "wait": true
60
+ },
61
+ "id": "2900d540-a3cd-488b-bc44-175b18575dcf",
62
+ "name": "Taifoon Devnet Signer",
63
+ "type": "CUSTOM.taifoonDevnetSigner",
64
+ "typeVersion": 1,
65
+ "position": [
66
+ 860,
67
+ 300
68
+ ],
69
+ "credentials": {
70
+ "taifoonDevnetKeyApi": {
71
+ "id": "",
72
+ "name": "REPLACE: taifoonDevnetKeyApi"
73
+ }
74
+ },
75
+ "notes": "in: verdict.json → out: verdict.json · sign and send the stamp on devnet 36927 (anvil #0)",
76
+ "notesInFlow": true
77
+ },
78
+ {
79
+ "parameters": {
80
+ "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').first().json };\ndelete v.stamp;\nreturn [{ json: { ...v, anchors: [...v.anchors, ...stampAnchors($json.sent, 36927)] } }];"
81
+ },
82
+ "id": "49dcb63d-9ae5-46e8-b7c3-abdc60598643",
83
+ "name": "Verdict + stamp",
84
+ "type": "n8n-nodes-base.code",
85
+ "typeVersion": 2,
86
+ "position": [
87
+ 1080,
88
+ 300
89
+ ],
90
+ "notes": "in: verdict.json → out: verdict.json · the same VERDICT with the stamp transaction appended to anchors",
91
+ "notesInFlow": true
92
+ }
93
+ ],
94
+ "connections": {
95
+ "Start": {
96
+ "main": [
97
+ [
98
+ {
99
+ "node": "Verdict",
100
+ "type": "main",
101
+ "index": 0
102
+ }
103
+ ]
104
+ ]
105
+ },
106
+ "Verdict": {
107
+ "main": [
108
+ [
109
+ {
110
+ "node": "Stamp Grade",
111
+ "type": "main",
112
+ "index": 0
113
+ }
114
+ ]
115
+ ]
116
+ },
117
+ "Stamp Grade": {
118
+ "main": [
119
+ [
120
+ {
121
+ "node": "Taifoon Devnet Signer",
122
+ "type": "main",
123
+ "index": 0
124
+ }
125
+ ]
126
+ ]
127
+ },
128
+ "Taifoon Devnet Signer": {
129
+ "main": [
130
+ [
131
+ {
132
+ "node": "Verdict + stamp",
133
+ "type": "main",
134
+ "index": 0
135
+ }
136
+ ]
137
+ ]
138
+ }
139
+ },
140
+ "settings": {},
141
+ "active": false,
142
+ "meta": {
143
+ "taifoon": {
144
+ "contract": "https://www.taifoon.io/schemas/coordination/v1/",
145
+ "steps": {
146
+ "Start": {
147
+ "in": null,
148
+ "out": null,
149
+ "does": "start (pass { decision_id } to stamp another verdict)"
150
+ },
151
+ "Verdict": {
152
+ "in": null,
153
+ "out": "https://www.taifoon.io/schemas/coordination/v1/verdict.json",
154
+ "does": "GET /v1/judge/decisions/:id → the canonical VERDICT (fromDecision) and its stamp code"
155
+ },
156
+ "Stamp Grade": {
157
+ "in": "https://www.taifoon.io/schemas/coordination/v1/verdict.json",
158
+ "out": "https://www.taifoon.io/schemas/coordination/v1/verdict.json",
159
+ "does": "POST /v1/assurance/call stampGrade: the unsigned GradeStampRegistry.stamp(subject_id, digest, verdict code, mode)"
160
+ },
161
+ "Taifoon Devnet Signer": {
162
+ "in": "https://www.taifoon.io/schemas/coordination/v1/verdict.json",
163
+ "out": "https://www.taifoon.io/schemas/coordination/v1/verdict.json",
164
+ "does": "sign and send the stamp on devnet 36927 (anvil #0)"
165
+ },
166
+ "Verdict + stamp": {
167
+ "in": "https://www.taifoon.io/schemas/coordination/v1/verdict.json",
168
+ "out": "https://www.taifoon.io/schemas/coordination/v1/verdict.json",
169
+ "does": "the same VERDICT with the stamp transaction appended to anchors"
170
+ }
171
+ }
172
+ }
173
+ },
174
+ "tags": []
175
+ }