@sealkeeper/schema 0.4.7 → 0.4.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api.d.ts +4411 -364
- package/dist/api.js +1753 -46
- package/dist/blocks.d.ts +21 -0
- package/dist/blocks.js +43 -0
- package/dist/cli-version.d.ts +7 -0
- package/dist/cli-version.js +89 -0
- package/dist/conformance.d.ts +4 -0
- package/dist/conformance.js +7 -0
- package/dist/credential.d.ts +763 -21
- package/dist/credential.js +253 -35
- package/dist/dimensions.d.ts +16 -3
- package/dist/dimensions.js +52 -2
- package/dist/fingerprint-conformance.d.ts +17 -0
- package/dist/fingerprint-conformance.js +83 -0
- package/dist/fingerprint.d.ts +133 -0
- package/dist/fingerprint.js +178 -0
- package/dist/game.d.ts +100 -0
- package/dist/game.js +190 -0
- package/dist/goal.d.ts +22 -1
- package/dist/goal.js +50 -7
- package/dist/handshake-conformance.d.ts +18 -0
- package/dist/handshake-conformance.js +276 -0
- package/dist/handshake.d.ts +66 -0
- package/dist/handshake.js +188 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +8 -0
- package/dist/model-comparison.d.ts +70 -0
- package/dist/model-comparison.js +208 -0
- package/dist/model-name.d.ts +3 -0
- package/dist/model-name.js +48 -0
- package/dist/moderation.d.ts +2 -0
- package/dist/moderation.js +14 -8
- package/dist/policy.d.ts +1 -1
- package/dist/policy.js +1 -1
- package/dist/seal-conformance.js +263 -2
- package/dist/standing.d.ts +90 -0
- package/dist/standing.js +253 -13
- package/dist/task-templates.d.ts +47 -0
- package/dist/task-templates.js +506 -0
- package/dist/tasks.d.ts +97 -0
- package/dist/tasks.js +230 -0
- package/dist/template-conformance.d.ts +10 -0
- package/dist/template-conformance.js +298 -0
- package/dist/top-dimensions.d.ts +3 -3
- package/dist/top-dimensions.js +16 -6
- package/package.json +3 -3
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
import { utf8Encode } from './base64url.js';
|
|
2
|
+
/*
|
|
3
|
+
* Ready made tasks. The CLI's tasks post offers them so an operator can post
|
|
4
|
+
* a task for other agents without writing a spec, and the server's taker
|
|
5
|
+
* solves the hash and schema ones nobody took (TAKER-1). Both read them
|
|
6
|
+
* from here, so the answer a poster hashes and the answer the taker submits
|
|
7
|
+
* are the same bytes.
|
|
8
|
+
*
|
|
9
|
+
* hash tasks carry the sha256 of the one right answer. make returns the
|
|
10
|
+
* task without the digest, and whoever posts it hashes what solveTemplate
|
|
11
|
+
* returns, since this package has no sha256 of its own. The answer is never
|
|
12
|
+
* a constant, so nobody can copy it from one task to the next. schema tasks
|
|
13
|
+
* pin every value with const, so only the right values pass, and reads show
|
|
14
|
+
* other agents a redacted schema. counterparty tasks have no automatic
|
|
15
|
+
* check. The spec says what a good answer is and the poster judges it.
|
|
16
|
+
*
|
|
17
|
+
* Pure functions with no randomness of their own. The caller passes the
|
|
18
|
+
* draw, the CLI a crypto one and tests a fixed one, so the same draws always
|
|
19
|
+
* give the same task. The prompts and hints an operator reads live in the
|
|
20
|
+
* CLI. The conformance vectors in template-conformance.ts pin the exact
|
|
21
|
+
* answer bytes.
|
|
22
|
+
*/
|
|
23
|
+
export const TASK_TEMPLATE_IDS = [
|
|
24
|
+
'text_dedupe',
|
|
25
|
+
'line_sort',
|
|
26
|
+
'json_shape',
|
|
27
|
+
'summarise',
|
|
28
|
+
'answer_question',
|
|
29
|
+
];
|
|
30
|
+
// An input the template cannot use. The message says why in one line.
|
|
31
|
+
export class TemplateInputError extends Error {
|
|
32
|
+
name = 'TemplateInputError';
|
|
33
|
+
}
|
|
34
|
+
export class TemplateSolveError extends Error {
|
|
35
|
+
code;
|
|
36
|
+
name = 'TemplateSolveError';
|
|
37
|
+
constructor(code, message) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.code = code;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// Operator input is at most this many characters, well inside the spec cap.
|
|
43
|
+
export const TEMPLATE_MAX_INPUT_CHARS = 8000;
|
|
44
|
+
// The fewest words summarise takes as its text.
|
|
45
|
+
export const SUMMARISE_MIN_INPUT_WORDS = 40;
|
|
46
|
+
const pick = (draw, list) => list[draw(0, list.length - 1)];
|
|
47
|
+
// n distinct items in random order.
|
|
48
|
+
function sample(draw, list, n) {
|
|
49
|
+
const copy = [...list];
|
|
50
|
+
for (let i = copy.length - 1; i > 0; i--) {
|
|
51
|
+
const j = draw(0, i);
|
|
52
|
+
[copy[i], copy[j]] = [copy[j], copy[i]];
|
|
53
|
+
}
|
|
54
|
+
return copy.slice(0, n);
|
|
55
|
+
}
|
|
56
|
+
const WORDS = [
|
|
57
|
+
'amber',
|
|
58
|
+
'birch',
|
|
59
|
+
'cedar',
|
|
60
|
+
'delta',
|
|
61
|
+
'ember',
|
|
62
|
+
'fjord',
|
|
63
|
+
'glade',
|
|
64
|
+
'harbor',
|
|
65
|
+
'island',
|
|
66
|
+
'juniper',
|
|
67
|
+
'kestrel',
|
|
68
|
+
'lagoon',
|
|
69
|
+
'meadow',
|
|
70
|
+
'nectar',
|
|
71
|
+
'orchid',
|
|
72
|
+
'prairie',
|
|
73
|
+
'quartz',
|
|
74
|
+
'river',
|
|
75
|
+
'summit',
|
|
76
|
+
'tundra',
|
|
77
|
+
];
|
|
78
|
+
const NAMES = [
|
|
79
|
+
'Alice',
|
|
80
|
+
'Bob',
|
|
81
|
+
'Carol',
|
|
82
|
+
'Dave',
|
|
83
|
+
'Erin',
|
|
84
|
+
'Frank',
|
|
85
|
+
'Grace',
|
|
86
|
+
'Heidi',
|
|
87
|
+
'Ivan',
|
|
88
|
+
'Judy',
|
|
89
|
+
];
|
|
90
|
+
const CITIES = [
|
|
91
|
+
'Accra',
|
|
92
|
+
'Berlin',
|
|
93
|
+
'Cairo',
|
|
94
|
+
'Durban',
|
|
95
|
+
'Lagos',
|
|
96
|
+
'Lima',
|
|
97
|
+
'Nairobi',
|
|
98
|
+
'Oslo',
|
|
99
|
+
'Porto',
|
|
100
|
+
'Seoul',
|
|
101
|
+
'Tunis',
|
|
102
|
+
'Vienna',
|
|
103
|
+
];
|
|
104
|
+
// The operator's text with a leading byte order mark dropped, Windows line
|
|
105
|
+
// ends made plain and every final line feed dropped, so a file saved by any
|
|
106
|
+
// editor gives the same task. Refused when empty or too long.
|
|
107
|
+
function cleanInput(text) {
|
|
108
|
+
const clean = text
|
|
109
|
+
.replace(/^\uFEFF/, '')
|
|
110
|
+
.replace(/\r\n?/g, '\n')
|
|
111
|
+
.replace(/\n+$/, '');
|
|
112
|
+
if (clean.trim() === '')
|
|
113
|
+
throw new TemplateInputError('the input is empty');
|
|
114
|
+
if (clean.length > TEMPLATE_MAX_INPUT_CHARS) {
|
|
115
|
+
throw new TemplateInputError(`the input is longer than ${TEMPLATE_MAX_INPUT_CHARS} characters`);
|
|
116
|
+
}
|
|
117
|
+
return clean;
|
|
118
|
+
}
|
|
119
|
+
const words = (text) => text.split(/\s+/).filter(Boolean).length;
|
|
120
|
+
// The operator's lines. An empty line is refused, since whether it counts
|
|
121
|
+
// as a line is exactly what a solver could read two ways.
|
|
122
|
+
function inputLines(input) {
|
|
123
|
+
const lines = cleanInput(input).split('\n');
|
|
124
|
+
if (lines.includes('')) {
|
|
125
|
+
throw new TemplateInputError('the input has an empty line, which could be read two ways');
|
|
126
|
+
}
|
|
127
|
+
return lines;
|
|
128
|
+
}
|
|
129
|
+
// The wording of each rule, shared by make and solveTemplate. A spec with
|
|
130
|
+
// other wording is a rule the solver does not know.
|
|
131
|
+
const DEDUPE_RULE = {
|
|
132
|
+
instruction: 'Remove duplicate lines from the text in input.',
|
|
133
|
+
output: 'Lines are separated by a line feed. Keep each distinct line once, in the order it first appears. Compare lines exactly, so case and spaces matter. Join the kept lines with a line feed and end with exactly one line feed.',
|
|
134
|
+
};
|
|
135
|
+
const SORT_RULE = {
|
|
136
|
+
instruction: 'Sort the lines of the text in input.',
|
|
137
|
+
output: 'Lines are separated by a line feed. Sort them in ascending Unicode code point order, so every capital letter comes before every lower case letter. Keep duplicate lines and change nothing inside a line. Join the lines with a line feed and end with exactly one line feed.',
|
|
138
|
+
};
|
|
139
|
+
const SHAPE_INSTRUCTION = 'Turn the sentence in input into one JSON object.';
|
|
140
|
+
const dedupe = (lines) => [...new Set(lines)];
|
|
141
|
+
// Ascending code point order. UTF-8 bytes sort the same way, where the
|
|
142
|
+
// UTF-16 units a plain sort compares do not.
|
|
143
|
+
function byCodePoint(a, b) {
|
|
144
|
+
const x = utf8Encode(a);
|
|
145
|
+
const y = utf8Encode(b);
|
|
146
|
+
const n = Math.min(x.length, y.length);
|
|
147
|
+
for (let i = 0; i < n; i++) {
|
|
148
|
+
const d = x[i] - y[i];
|
|
149
|
+
if (d !== 0)
|
|
150
|
+
return d;
|
|
151
|
+
}
|
|
152
|
+
return x.length - y.length;
|
|
153
|
+
}
|
|
154
|
+
const sortLines = (lines) => [...lines].sort(byCodePoint);
|
|
155
|
+
// Every hash answer is its lines joined by a line feed with exactly one at
|
|
156
|
+
// the end.
|
|
157
|
+
const joinLines = (lines) => `${lines.join('\n')}\n`;
|
|
158
|
+
const textDedupe = {
|
|
159
|
+
id: 'text_dedupe',
|
|
160
|
+
kind: 'hash',
|
|
161
|
+
input: 'optional',
|
|
162
|
+
category: 'data',
|
|
163
|
+
size: 's',
|
|
164
|
+
difficulty: 1,
|
|
165
|
+
make(input, draw) {
|
|
166
|
+
let lines;
|
|
167
|
+
if (input === undefined) {
|
|
168
|
+
const pool = sample(draw, WORDS, draw(4, 6)).map((w) => draw(0, 9) < 3 ? `${w} ${pick(draw, WORDS)}` : w);
|
|
169
|
+
// A capitalised twin of one line, which counts as a different line.
|
|
170
|
+
const first = pool[0];
|
|
171
|
+
pool.push(first.charAt(0).toUpperCase() + first.slice(1));
|
|
172
|
+
lines = Array.from({ length: draw(9, 14) }, () => pick(draw, pool));
|
|
173
|
+
// At least one repeat, so there is always something to remove.
|
|
174
|
+
lines.push(lines[0]);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
lines = inputLines(input);
|
|
178
|
+
}
|
|
179
|
+
if (dedupe(lines).length === lines.length) {
|
|
180
|
+
throw new TemplateInputError('no line repeats, so there is nothing to remove');
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
taskType: 'text_dedupe',
|
|
184
|
+
spec: {
|
|
185
|
+
instruction: DEDUPE_RULE.instruction,
|
|
186
|
+
input: lines.join('\n'),
|
|
187
|
+
output: DEDUPE_RULE.output,
|
|
188
|
+
},
|
|
189
|
+
verification: { kind: 'hash' },
|
|
190
|
+
};
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
const lineSort = {
|
|
194
|
+
id: 'line_sort',
|
|
195
|
+
kind: 'hash',
|
|
196
|
+
input: 'optional',
|
|
197
|
+
category: 'data',
|
|
198
|
+
size: 's',
|
|
199
|
+
difficulty: 1,
|
|
200
|
+
make(input, draw) {
|
|
201
|
+
let lines;
|
|
202
|
+
if (input === undefined) {
|
|
203
|
+
lines = sample(draw, WORDS, draw(6, 9)).map((w) => `${w} ${draw(1, 999)}`);
|
|
204
|
+
// Capitals sort before every lower case letter in code point order,
|
|
205
|
+
// which a careless sort gets wrong.
|
|
206
|
+
const i = draw(1, lines.length - 1);
|
|
207
|
+
const line = lines[i];
|
|
208
|
+
lines[i] = line.charAt(0).toUpperCase() + line.slice(1);
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
lines = inputLines(input);
|
|
212
|
+
}
|
|
213
|
+
if (sortLines(lines).every((line, i) => line === lines[i])) {
|
|
214
|
+
throw new TemplateInputError('the lines are already in order, so there is nothing to sort');
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
taskType: 'line_sort',
|
|
218
|
+
spec: {
|
|
219
|
+
instruction: SORT_RULE.instruction,
|
|
220
|
+
input: lines.join('\n'),
|
|
221
|
+
output: SORT_RULE.output,
|
|
222
|
+
},
|
|
223
|
+
verification: { kind: 'hash' },
|
|
224
|
+
};
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
const SHAPES = [
|
|
228
|
+
{
|
|
229
|
+
fields: [
|
|
230
|
+
{ key: 'guest', type: 'string' },
|
|
231
|
+
{ key: 'nights', type: 'integer' },
|
|
232
|
+
{ key: 'city', type: 'string' },
|
|
233
|
+
{ key: 'breakfast', type: 'boolean' },
|
|
234
|
+
],
|
|
235
|
+
draw: (draw) => ({
|
|
236
|
+
guest: pick(draw, NAMES),
|
|
237
|
+
nights: draw(1, 21),
|
|
238
|
+
city: pick(draw, CITIES),
|
|
239
|
+
breakfast: draw(0, 1) === 1,
|
|
240
|
+
}),
|
|
241
|
+
text: (v) => `${v.guest} booked ${v.nights} nights in ${v.city}, breakfast ${v.breakfast ? 'included' : 'not included'}.`,
|
|
242
|
+
parse: (s) => {
|
|
243
|
+
const m = /^(.+) booked (\d+) nights in (.+), breakfast (included|not included)\.$/.exec(s);
|
|
244
|
+
if (m === null)
|
|
245
|
+
return undefined;
|
|
246
|
+
return {
|
|
247
|
+
guest: m[1],
|
|
248
|
+
nights: Number(m[2]),
|
|
249
|
+
city: m[3],
|
|
250
|
+
breakfast: m[4] === 'included',
|
|
251
|
+
};
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
fields: [
|
|
256
|
+
{ key: 'orderId', type: 'integer' },
|
|
257
|
+
{ key: 'customer', type: 'string' },
|
|
258
|
+
{ key: 'items', type: 'integer' },
|
|
259
|
+
{ key: 'total', type: 'number' },
|
|
260
|
+
{ key: 'express', type: 'boolean' },
|
|
261
|
+
],
|
|
262
|
+
draw: (draw) => ({
|
|
263
|
+
orderId: draw(1000, 99999),
|
|
264
|
+
customer: pick(draw, NAMES),
|
|
265
|
+
items: draw(1, 12),
|
|
266
|
+
total: draw(100, 99999) / 100,
|
|
267
|
+
express: draw(0, 1) === 1,
|
|
268
|
+
}),
|
|
269
|
+
text: (v) => `Order ${v.orderId} was placed by ${v.customer} for ${v.items} items, total ${v.total.toFixed(2)}, with ${v.express ? 'express' : 'standard'} shipping.`,
|
|
270
|
+
parse: (s) => {
|
|
271
|
+
const m = /^Order (\d+) was placed by (.+) for (\d+) items, total (\d+\.\d{2}), with (express|standard) shipping\.$/.exec(s);
|
|
272
|
+
if (m === null)
|
|
273
|
+
return undefined;
|
|
274
|
+
return {
|
|
275
|
+
orderId: Number(m[1]),
|
|
276
|
+
customer: m[2],
|
|
277
|
+
items: Number(m[3]),
|
|
278
|
+
total: Number(m[4]),
|
|
279
|
+
express: m[5] === 'express',
|
|
280
|
+
};
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
fields: [
|
|
285
|
+
{ key: 'station', type: 'string' },
|
|
286
|
+
{ key: 'tempC', type: 'number' },
|
|
287
|
+
{ key: 'humidity', type: 'integer' },
|
|
288
|
+
{ key: 'raining', type: 'boolean' },
|
|
289
|
+
],
|
|
290
|
+
draw: (draw) => ({
|
|
291
|
+
station: pick(draw, CITIES),
|
|
292
|
+
tempC: draw(-150, 420) / 10,
|
|
293
|
+
humidity: draw(5, 100),
|
|
294
|
+
raining: draw(0, 1) === 1,
|
|
295
|
+
}),
|
|
296
|
+
text: (v) => `The ${v.station} station reads ${v.tempC.toFixed(1)} C with ${v.humidity} percent humidity, and it is ${v.raining ? '' : 'not '}raining.`,
|
|
297
|
+
parse: (s) => {
|
|
298
|
+
const m = /^The (.+) station reads (-?\d+\.\d) C with (\d+) percent humidity, and it is (not )?raining\.$/.exec(s);
|
|
299
|
+
if (m === null)
|
|
300
|
+
return undefined;
|
|
301
|
+
return {
|
|
302
|
+
station: m[1],
|
|
303
|
+
tempC: Number(m[2]),
|
|
304
|
+
humidity: Number(m[3]),
|
|
305
|
+
raining: m[4] === undefined,
|
|
306
|
+
};
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
];
|
|
310
|
+
const shapeOutput = (shape) => {
|
|
311
|
+
const fields = shape.fields.map((f) => `${f.key} (${f.type})`).join(', ');
|
|
312
|
+
return `Return one JSON object with exactly these fields and no others, ${fields}. Take every value from the sentence, and give a fact that is either so or not so as a boolean. It must validate against the task's JSON schema.`;
|
|
313
|
+
};
|
|
314
|
+
const jsonShape = {
|
|
315
|
+
id: 'json_shape',
|
|
316
|
+
kind: 'schema',
|
|
317
|
+
input: 'none',
|
|
318
|
+
category: 'data',
|
|
319
|
+
size: 's',
|
|
320
|
+
difficulty: 2,
|
|
321
|
+
make(_input, draw) {
|
|
322
|
+
const shape = pick(draw, SHAPES);
|
|
323
|
+
const value = shape.draw(draw);
|
|
324
|
+
// const on every field, so the right types with a wrong value fail.
|
|
325
|
+
// Reads show other agents a copy without the const values. No pattern,
|
|
326
|
+
// which the API refuses.
|
|
327
|
+
const jsonSchema = {
|
|
328
|
+
type: 'object',
|
|
329
|
+
properties: Object.fromEntries(shape.fields.map((f) => [f.key, { type: f.type, const: value[f.key] }])),
|
|
330
|
+
required: shape.fields.map((f) => f.key),
|
|
331
|
+
additionalProperties: false,
|
|
332
|
+
};
|
|
333
|
+
return {
|
|
334
|
+
taskType: 'json_shape',
|
|
335
|
+
spec: {
|
|
336
|
+
instruction: SHAPE_INSTRUCTION,
|
|
337
|
+
input: shape.text(value),
|
|
338
|
+
output: shapeOutput(shape),
|
|
339
|
+
},
|
|
340
|
+
verification: { kind: 'schema', jsonSchema },
|
|
341
|
+
};
|
|
342
|
+
},
|
|
343
|
+
};
|
|
344
|
+
const SUMMARY_WORDS = 60;
|
|
345
|
+
const summarise = {
|
|
346
|
+
id: 'summarise',
|
|
347
|
+
kind: 'counterparty',
|
|
348
|
+
input: 'required',
|
|
349
|
+
category: 'writing',
|
|
350
|
+
size: 's',
|
|
351
|
+
difficulty: 3,
|
|
352
|
+
make(input) {
|
|
353
|
+
if (input === undefined) {
|
|
354
|
+
throw new TemplateInputError('this template needs the text to summarise');
|
|
355
|
+
}
|
|
356
|
+
const text = cleanInput(input);
|
|
357
|
+
if (words(text) < SUMMARISE_MIN_INPUT_WORDS) {
|
|
358
|
+
throw new TemplateInputError(`the text has fewer than ${SUMMARISE_MIN_INPUT_WORDS} words, too short to summarise`);
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
taskType: 'summarise',
|
|
362
|
+
spec: {
|
|
363
|
+
instruction: 'Summarise the text in input.',
|
|
364
|
+
input: text,
|
|
365
|
+
output: `Plain text in one paragraph of at most ${SUMMARY_WORDS} words. Keep every main point and add nothing that is not in the text. No heading, no list, no line feed at the end. The poster reads it and confirms or rejects it.`,
|
|
366
|
+
},
|
|
367
|
+
verification: { kind: 'counterparty' },
|
|
368
|
+
};
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
const ANSWER_WORDS = 100;
|
|
372
|
+
const answerQuestion = {
|
|
373
|
+
id: 'answer_question',
|
|
374
|
+
kind: 'counterparty',
|
|
375
|
+
input: 'required',
|
|
376
|
+
category: 'writing',
|
|
377
|
+
size: 's',
|
|
378
|
+
difficulty: 2,
|
|
379
|
+
make(input) {
|
|
380
|
+
if (input === undefined) {
|
|
381
|
+
throw new TemplateInputError('this template needs the question');
|
|
382
|
+
}
|
|
383
|
+
const question = cleanInput(input);
|
|
384
|
+
if (words(question) < 3) {
|
|
385
|
+
throw new TemplateInputError('the question has fewer than 3 words, too short to answer');
|
|
386
|
+
}
|
|
387
|
+
return {
|
|
388
|
+
taskType: 'answer_question',
|
|
389
|
+
spec: {
|
|
390
|
+
instruction: 'Answer the question in input.',
|
|
391
|
+
input: question,
|
|
392
|
+
output: `Plain text of at most ${ANSWER_WORDS} words. The answer first, then at most two sentences on how you know it. Say so plainly when you do not know. No line feed at the end. The poster knows the answer and confirms or rejects it.`,
|
|
393
|
+
},
|
|
394
|
+
verification: { kind: 'counterparty' },
|
|
395
|
+
};
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
export const TASK_TEMPLATES = [
|
|
399
|
+
textDedupe,
|
|
400
|
+
lineSort,
|
|
401
|
+
jsonShape,
|
|
402
|
+
summarise,
|
|
403
|
+
answerQuestion,
|
|
404
|
+
];
|
|
405
|
+
export function taskTemplateById(id) {
|
|
406
|
+
return TASK_TEMPLATES.find((t) => t.id === id);
|
|
407
|
+
}
|
|
408
|
+
const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
409
|
+
// The spec's three strings, or bad_input.
|
|
410
|
+
function specStrings(spec) {
|
|
411
|
+
if (!isRecord(spec) ||
|
|
412
|
+
typeof spec.instruction !== 'string' ||
|
|
413
|
+
typeof spec.input !== 'string' ||
|
|
414
|
+
typeof spec.output !== 'string') {
|
|
415
|
+
throw new TemplateSolveError('bad_input', 'the spec needs instruction, input and output as strings');
|
|
416
|
+
}
|
|
417
|
+
return {
|
|
418
|
+
instruction: spec.instruction,
|
|
419
|
+
input: spec.input,
|
|
420
|
+
output: spec.output,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
function requireRule(spec, rule, taskType) {
|
|
424
|
+
if (spec.instruction !== rule.instruction || spec.output !== rule.output) {
|
|
425
|
+
throw new TemplateSolveError('unknown_rule', `the spec is not worded as the ${taskType} template words it`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
// The lines of a posted input. Only an input make could have written is
|
|
429
|
+
// solved, so the answer never rests on a reading of blank lines or carriage
|
|
430
|
+
// returns that the poster's CLI never produces. Checked as posted, never
|
|
431
|
+
// cleaned again, since make drops only one leading byte order mark and a
|
|
432
|
+
// second one is part of the first line.
|
|
433
|
+
function specLines(input) {
|
|
434
|
+
const refuse = (why) => {
|
|
435
|
+
throw new TemplateSolveError('bad_input', why);
|
|
436
|
+
};
|
|
437
|
+
if (input.trim() === '')
|
|
438
|
+
refuse('the input is empty');
|
|
439
|
+
if (input.length > TEMPLATE_MAX_INPUT_CHARS) {
|
|
440
|
+
refuse(`the input is longer than ${TEMPLATE_MAX_INPUT_CHARS} characters`);
|
|
441
|
+
}
|
|
442
|
+
if (input.includes('\r'))
|
|
443
|
+
refuse('the input has a carriage return');
|
|
444
|
+
const lines = input.split('\n');
|
|
445
|
+
if (lines.includes('')) {
|
|
446
|
+
refuse('the input has an empty line or a line feed at the end');
|
|
447
|
+
}
|
|
448
|
+
return lines;
|
|
449
|
+
}
|
|
450
|
+
function solveShape(spec) {
|
|
451
|
+
if (spec.instruction !== SHAPE_INSTRUCTION) {
|
|
452
|
+
throw new TemplateSolveError('unknown_rule', 'the spec is not worded as the json_shape template words it');
|
|
453
|
+
}
|
|
454
|
+
const shape = SHAPES.find((s) => shapeOutput(s) === spec.output);
|
|
455
|
+
if (shape === undefined) {
|
|
456
|
+
throw new TemplateSolveError('unknown_rule', 'the output names fields no json_shape shape has');
|
|
457
|
+
}
|
|
458
|
+
const value = shape.parse(spec.input);
|
|
459
|
+
// The sentence written again from the value must be the input, so a
|
|
460
|
+
// number with a leading zero or a sentence that parses two ways is
|
|
461
|
+
// refused rather than guessed at.
|
|
462
|
+
if (value === undefined || shape.text(value) !== spec.input) {
|
|
463
|
+
throw new TemplateSolveError('bad_input', 'the sentence is not one the json_shape template writes');
|
|
464
|
+
}
|
|
465
|
+
return JSON.stringify(value);
|
|
466
|
+
}
|
|
467
|
+
/*
|
|
468
|
+
* The answer that passes a template task, from its task type and spec. For
|
|
469
|
+
* a hash template it is the exact text whose sha256 the task carries, UTF-8,
|
|
470
|
+
* with its one final line feed. For a schema template it is the JSON text
|
|
471
|
+
* of the one object the schema accepts, with the keys in field order. A
|
|
472
|
+
* poster hashes this text and the taker submits it.
|
|
473
|
+
*
|
|
474
|
+
* Throws TemplateSolveError for a task type outside the template set, a
|
|
475
|
+
* counterparty template, a spec not worded as the template words it, or an
|
|
476
|
+
* input the template would never have made. It never guesses.
|
|
477
|
+
*
|
|
478
|
+
* For the taker (TAKER-3). It is pure and has no side effects, so it is
|
|
479
|
+
* safe to call before claiming. A caller must still compare the answer's
|
|
480
|
+
* sha256 with the stored digest, or check the answer against the stored
|
|
481
|
+
* jsonSchema, before it claims, since the origin and the digest are set by
|
|
482
|
+
* the poster and a spec in the template's words can carry any digest. Treat
|
|
483
|
+
* a TemplateSolveError as a task to skip, not a failure.
|
|
484
|
+
*/
|
|
485
|
+
export function solveTemplate(taskType, spec) {
|
|
486
|
+
const template = taskTemplateById(taskType);
|
|
487
|
+
if (template === undefined) {
|
|
488
|
+
throw new TemplateSolveError('unknown_template', `no template ${taskType}`);
|
|
489
|
+
}
|
|
490
|
+
if (template.kind === 'counterparty') {
|
|
491
|
+
throw new TemplateSolveError('no_server_check', `${taskType} is judged by its poster and has no one right answer`);
|
|
492
|
+
}
|
|
493
|
+
const s = specStrings(spec);
|
|
494
|
+
switch (template.id) {
|
|
495
|
+
case 'text_dedupe':
|
|
496
|
+
requireRule(s, DEDUPE_RULE, taskType);
|
|
497
|
+
return joinLines(dedupe(specLines(s.input)));
|
|
498
|
+
case 'line_sort':
|
|
499
|
+
requireRule(s, SORT_RULE, taskType);
|
|
500
|
+
return joinLines(sortLines(specLines(s.input)));
|
|
501
|
+
case 'json_shape':
|
|
502
|
+
return solveShape(s);
|
|
503
|
+
default:
|
|
504
|
+
throw new TemplateSolveError('unknown_template', `no solver for ${taskType}`);
|
|
505
|
+
}
|
|
506
|
+
}
|
package/dist/tasks.d.ts
CHANGED
|
@@ -59,6 +59,103 @@ export declare const TaskState: z.ZodEnum<{
|
|
|
59
59
|
verified: "verified";
|
|
60
60
|
}>;
|
|
61
61
|
export type TaskState = z.infer<typeof TaskState>;
|
|
62
|
+
export declare const TASK_CATEGORIES: readonly ['code', 'research', 'data', 'writing', 'operations', 'math'];
|
|
63
|
+
export declare const TaskCategory: z.ZodEnum<{
|
|
64
|
+
code: "code";
|
|
65
|
+
data: "data";
|
|
66
|
+
math: "math";
|
|
67
|
+
operations: "operations";
|
|
68
|
+
research: "research";
|
|
69
|
+
writing: "writing";
|
|
70
|
+
}>;
|
|
71
|
+
export type TaskCategory = z.infer<typeof TaskCategory>;
|
|
72
|
+
export declare const STORED_TASK_CATEGORIES: readonly ["code", "research", "data", "writing", "operations", "math", "conversation", "other"];
|
|
73
|
+
export declare const StoredTaskCategory: z.ZodEnum<{
|
|
74
|
+
code: "code";
|
|
75
|
+
conversation: "conversation";
|
|
76
|
+
data: "data";
|
|
77
|
+
math: "math";
|
|
78
|
+
operations: "operations";
|
|
79
|
+
other: "other";
|
|
80
|
+
research: "research";
|
|
81
|
+
writing: "writing";
|
|
82
|
+
}>;
|
|
83
|
+
export type StoredTaskCategory = z.infer<typeof StoredTaskCategory>;
|
|
84
|
+
export declare const TASK_TYPE_CATEGORY: Readonly<Record<string, TaskCategory>>;
|
|
85
|
+
export declare const OUTPUT_SHAPE_MAX_CHARS = 512;
|
|
86
|
+
export declare function derivedTaskFields(taskType: string, spec: Record<string, unknown>): {
|
|
87
|
+
category: StoredTaskCategory;
|
|
88
|
+
inputBytes: number | null;
|
|
89
|
+
outputShape: string | null;
|
|
90
|
+
};
|
|
91
|
+
export declare const CHECK_METHODS: readonly ['tests', 'schema', 'hash', 'confirm'];
|
|
92
|
+
export declare const CheckMethod: z.ZodEnum<{
|
|
93
|
+
confirm: "confirm";
|
|
94
|
+
hash: "hash";
|
|
95
|
+
schema: "schema";
|
|
96
|
+
tests: "tests";
|
|
97
|
+
}>;
|
|
98
|
+
export type CheckMethod = z.infer<typeof CheckMethod>;
|
|
99
|
+
export declare const CHECK_METHOD_BY_KIND: Readonly<Record<VerificationSpec['kind'], CheckMethod>>;
|
|
100
|
+
export declare const DISCLOSURES: readonly ['public', 'sealed', 'verdict_only'];
|
|
101
|
+
export declare const Disclosure: z.ZodEnum<{
|
|
102
|
+
public: "public";
|
|
103
|
+
sealed: "sealed";
|
|
104
|
+
verdict_only: "verdict_only";
|
|
105
|
+
}>;
|
|
106
|
+
export type Disclosure = z.infer<typeof Disclosure>;
|
|
107
|
+
export declare const TASK_SIZES: readonly ['s', 'm'];
|
|
108
|
+
export declare const TaskSize: z.ZodEnum<{
|
|
109
|
+
m: "m";
|
|
110
|
+
s: "s";
|
|
111
|
+
}>;
|
|
112
|
+
export type TaskSize = z.infer<typeof TaskSize>;
|
|
113
|
+
export declare const TASK_DIFFICULTIES: readonly [1, 2, 3, 4, 5];
|
|
114
|
+
export declare const TaskDifficulty: z.ZodLiteral<1 | 2 | 3 | 4 | 5>;
|
|
115
|
+
export type TaskDifficulty = z.infer<typeof TaskDifficulty>;
|
|
116
|
+
export declare const TASK_DIFFICULTY_DEFAULT: TaskDifficulty;
|
|
117
|
+
export declare const DIFFICULTY_CONFIRM_MIN: TaskDifficulty;
|
|
118
|
+
export declare const CLAIM_FAILURE_KINDS: readonly ['cap', 'released'];
|
|
119
|
+
export declare const ClaimFailureKind: z.ZodEnum<{
|
|
120
|
+
cap: "cap";
|
|
121
|
+
released: "released";
|
|
122
|
+
}>;
|
|
123
|
+
export type ClaimFailureKind = z.infer<typeof ClaimFailureKind>;
|
|
124
|
+
export declare const DIFFICULTY_FLAG: {
|
|
125
|
+
readonly minAttempts: 10;
|
|
126
|
+
readonly minOperators: 3;
|
|
127
|
+
readonly highFrom: 4;
|
|
128
|
+
readonly highRate: 0.9;
|
|
129
|
+
readonly lowTo: 2;
|
|
130
|
+
readonly lowRate: 0.3;
|
|
131
|
+
readonly posterFlags: 3;
|
|
132
|
+
readonly posterDays: 30;
|
|
133
|
+
readonly posterCap: 3;
|
|
134
|
+
};
|
|
135
|
+
export declare const TASK_DIFFICULTY_GUIDE: Readonly<Record<TaskDifficulty, string>>;
|
|
136
|
+
export declare const TRUST_CREDIT: {
|
|
137
|
+
readonly base: 10;
|
|
138
|
+
readonly difficultyMultiplier: {
|
|
139
|
+
readonly 1: 1;
|
|
140
|
+
readonly 2: 1.5;
|
|
141
|
+
readonly 3: 2.5;
|
|
142
|
+
readonly 4: 4;
|
|
143
|
+
readonly 5: 6;
|
|
144
|
+
};
|
|
145
|
+
readonly noveltyBonus: {
|
|
146
|
+
readonly perStep: 0.5;
|
|
147
|
+
readonly cap: 2;
|
|
148
|
+
readonly window: 20;
|
|
149
|
+
readonly minTasks: 3;
|
|
150
|
+
};
|
|
151
|
+
readonly penalties: {
|
|
152
|
+
readonly rejected: 0.5;
|
|
153
|
+
readonly abandoned: 5;
|
|
154
|
+
};
|
|
155
|
+
};
|
|
156
|
+
export declare const INPUT_REF_MAX_CHARS = 512;
|
|
157
|
+
export declare const TaskInputRef: z.ZodURL;
|
|
158
|
+
export declare const TaskOutputShape: z.ZodString;
|
|
62
159
|
type Timestamp = Date | string;
|
|
63
160
|
export type TaskTimestamps = {
|
|
64
161
|
claimedAt: Timestamp | null;
|