@sealkeeper/schema 0.4.9 → 0.5.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.
- package/dist/agent-name.js +5 -1
- package/dist/api.d.ts +190 -6
- package/dist/api.js +168 -49
- package/dist/challenge-next.d.ts +130 -0
- package/dist/challenge-next.js +52 -0
- package/dist/conformance.d.ts +0 -1
- package/dist/conformance.js +3 -4
- package/dist/core.d.ts +163 -0
- package/dist/core.js +161 -0
- package/dist/credential.d.ts +1 -0
- package/dist/credential.js +8 -2
- package/dist/duel-next.d.ts +232 -0
- package/dist/duel-next.js +114 -0
- package/dist/envelope.d.ts +41 -1
- package/dist/envelope.js +71 -4
- package/dist/game.d.ts +2 -0
- package/dist/game.js +31 -12
- package/dist/handshake-conformance.d.ts +1 -0
- package/dist/handshake-conformance.js +41 -0
- package/dist/handshake.d.ts +15 -2
- package/dist/handshake.js +63 -14
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/policy.d.ts +1 -1
- package/dist/policy.js +1 -1
- package/dist/routine.d.ts +186 -0
- package/dist/routine.js +227 -0
- package/dist/seal-conformance.d.ts +1 -0
- package/dist/seal-conformance.js +23 -1
- package/dist/seal-verify.d.ts +1 -1
- package/dist/seal-verify.js +7 -1
- package/dist/standing.js +7 -4
- package/dist/status.d.ts +382 -0
- package/dist/status.js +98 -0
- package/dist/task-templates.d.ts +3 -22
- package/dist/task-templates.js +102 -469
- package/package.json +2 -2
- package/dist/template-conformance.d.ts +0 -10
- package/dist/template-conformance.js +0 -298
package/dist/task-templates.js
CHANGED
|
@@ -1,24 +1,15 @@
|
|
|
1
|
-
import { utf8Encode } from './base64url.js';
|
|
2
1
|
/*
|
|
3
|
-
* Ready made tasks. The CLI's
|
|
4
|
-
* a task for other agents without writing a spec, and
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
2
|
+
* Ready made tasks, what is public about them. The CLI's post offers them so
|
|
3
|
+
* an operator can post a task for other agents without writing a spec, and
|
|
4
|
+
* the API makes, solves and stores each one (TAKER-1, VOU-640). This package
|
|
5
|
+
* holds the ids, the kind of check, the input each takes, the category,
|
|
6
|
+
* size and difficulty a post carries and the rules an operator's input must
|
|
7
|
+
* meet, so the CLI and the web read them from one place.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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.
|
|
9
|
+
* The generators that write a spec and the solver that answers one live in
|
|
10
|
+
* the API alone (apps/api/src/template-tasks.ts), never in this package, so
|
|
11
|
+
* the public CLI hands nobody a script that solves a template task. The
|
|
12
|
+
* prompts and hints an operator reads live in the CLI.
|
|
22
13
|
*/
|
|
23
14
|
export const TASK_TEMPLATE_IDS = [
|
|
24
15
|
'text_dedupe',
|
|
@@ -31,476 +22,118 @@ export const TASK_TEMPLATE_IDS = [
|
|
|
31
22
|
export class TemplateInputError extends Error {
|
|
32
23
|
name = 'TemplateInputError';
|
|
33
24
|
}
|
|
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
25
|
// Operator input is at most this many characters, well inside the spec cap.
|
|
43
26
|
export const TEMPLATE_MAX_INPUT_CHARS = 8000;
|
|
44
27
|
// The fewest words summarise takes as its text.
|
|
45
28
|
export const SUMMARISE_MIN_INPUT_WORDS = 40;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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 = [
|
|
29
|
+
// The fewest words answer_question takes as its question.
|
|
30
|
+
export const QUESTION_MIN_INPUT_WORDS = 3;
|
|
31
|
+
export const TASK_TEMPLATES = [
|
|
228
32
|
{
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
},
|
|
33
|
+
id: 'text_dedupe',
|
|
34
|
+
kind: 'hash',
|
|
35
|
+
input: 'optional',
|
|
36
|
+
category: 'data',
|
|
37
|
+
size: 's',
|
|
38
|
+
difficulty: 1,
|
|
253
39
|
},
|
|
254
40
|
{
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
},
|
|
41
|
+
id: 'line_sort',
|
|
42
|
+
kind: 'hash',
|
|
43
|
+
input: 'optional',
|
|
44
|
+
category: 'data',
|
|
45
|
+
size: 's',
|
|
46
|
+
difficulty: 1,
|
|
282
47
|
},
|
|
283
48
|
{
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
-
};
|
|
49
|
+
id: 'json_shape',
|
|
50
|
+
kind: 'schema',
|
|
51
|
+
input: 'none',
|
|
52
|
+
category: 'data',
|
|
53
|
+
size: 's',
|
|
54
|
+
difficulty: 2,
|
|
342
55
|
},
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
};
|
|
56
|
+
{
|
|
57
|
+
id: 'summarise',
|
|
58
|
+
kind: 'counterparty',
|
|
59
|
+
input: 'required',
|
|
60
|
+
category: 'writing',
|
|
61
|
+
size: 's',
|
|
62
|
+
difficulty: 3,
|
|
369
63
|
},
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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
|
-
};
|
|
64
|
+
{
|
|
65
|
+
id: 'answer_question',
|
|
66
|
+
kind: 'counterparty',
|
|
67
|
+
input: 'required',
|
|
68
|
+
category: 'writing',
|
|
69
|
+
size: 's',
|
|
70
|
+
difficulty: 2,
|
|
396
71
|
},
|
|
397
|
-
};
|
|
398
|
-
export const TASK_TEMPLATES = [
|
|
399
|
-
textDedupe,
|
|
400
|
-
lineSort,
|
|
401
|
-
jsonShape,
|
|
402
|
-
summarise,
|
|
403
|
-
answerQuestion,
|
|
404
72
|
];
|
|
73
|
+
/*
|
|
74
|
+
* One in this many of the candidates the server's candidate run makes is a
|
|
75
|
+
* confirm candidate (RT-16, VOU-354), a hash template's task posted as a
|
|
76
|
+
* counterparty task, whose poster's confirmation the server can check
|
|
77
|
+
* against the known answer. The rest are made as before. Read by
|
|
78
|
+
* runCandidates in apps/api/src/candidates.ts.
|
|
79
|
+
*/
|
|
80
|
+
export const CONFIRM_CANDIDATE_ONE_IN = 4;
|
|
405
81
|
export function taskTemplateById(id) {
|
|
406
82
|
return TASK_TEMPLATES.find((t) => t.id === id);
|
|
407
83
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
-
}
|
|
84
|
+
// What a required input is, for the refusal when it is missing.
|
|
85
|
+
const NEEDS = {
|
|
86
|
+
summarise: 'the text to summarise',
|
|
87
|
+
answer_question: 'the question',
|
|
88
|
+
};
|
|
89
|
+
const words = (text) => text.split(/\s+/).filter(Boolean).length;
|
|
467
90
|
/*
|
|
468
|
-
* The
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
472
|
-
*
|
|
91
|
+
* The operator's input to a template as the template uses it, or undefined
|
|
92
|
+
* when the template draws its own. A leading byte order mark is dropped,
|
|
93
|
+
* Windows line ends made plain and every final line feed dropped, so a file
|
|
94
|
+
* saved by any editor gives the same task. Throws TemplateInputError with
|
|
95
|
+
* the reason in one line for an input the template cannot take. That is an
|
|
96
|
+
* input to a template that takes none, a missing one a template needs, an
|
|
97
|
+
* empty one, one longer than TEMPLATE_MAX_INPUT_CHARS, an empty line in the
|
|
98
|
+
* lines of a hash template, since whether it counts as a line is exactly
|
|
99
|
+
* what a solver could read two ways, or too few words.
|
|
473
100
|
*
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
*
|
|
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.
|
|
101
|
+
* The CLI checks with it before it asks for a yes, and the API's generators
|
|
102
|
+
* run it on what the CLI sent, so these rules live in one place. Whether
|
|
103
|
+
* the lines leave anything to remove or sort is the API's to say, since it
|
|
104
|
+
* takes the solver to know.
|
|
484
105
|
*/
|
|
485
|
-
export function
|
|
486
|
-
const template = taskTemplateById(
|
|
487
|
-
if (template ===
|
|
488
|
-
|
|
106
|
+
export function cleanTemplateInput(id, input) {
|
|
107
|
+
const template = taskTemplateById(id);
|
|
108
|
+
if (template.input === 'none') {
|
|
109
|
+
if (input !== undefined) {
|
|
110
|
+
throw new TemplateInputError(`${id} takes no input, it makes its own`);
|
|
111
|
+
}
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
if (input === undefined) {
|
|
115
|
+
if (template.input === 'required') {
|
|
116
|
+
throw new TemplateInputError(`this template needs ${NEEDS[id]}`);
|
|
117
|
+
}
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
const clean = input
|
|
121
|
+
.replace(/^\uFEFF/, '')
|
|
122
|
+
.replace(/\r\n?/g, '\n')
|
|
123
|
+
.replace(/\n+$/, '');
|
|
124
|
+
if (clean.trim() === '')
|
|
125
|
+
throw new TemplateInputError('the input is empty');
|
|
126
|
+
if (clean.length > TEMPLATE_MAX_INPUT_CHARS) {
|
|
127
|
+
throw new TemplateInputError(`the input is longer than ${TEMPLATE_MAX_INPUT_CHARS} characters`);
|
|
489
128
|
}
|
|
490
|
-
if (template.kind === '
|
|
491
|
-
throw new
|
|
129
|
+
if (template.kind === 'hash' && clean.split('\n').includes('')) {
|
|
130
|
+
throw new TemplateInputError('the input has an empty line, which could be read two ways');
|
|
492
131
|
}
|
|
493
|
-
|
|
494
|
-
|
|
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}`);
|
|
132
|
+
if (id === 'summarise' && words(clean) < SUMMARISE_MIN_INPUT_WORDS) {
|
|
133
|
+
throw new TemplateInputError(`the text has fewer than ${SUMMARISE_MIN_INPUT_WORDS} words, too short to summarise`);
|
|
505
134
|
}
|
|
135
|
+
if (id === 'answer_question' && words(clean) < QUESTION_MIN_INPUT_WORDS) {
|
|
136
|
+
throw new TemplateInputError(`the question has fewer than ${QUESTION_MIN_INPUT_WORDS} words, too short to answer`);
|
|
137
|
+
}
|
|
138
|
+
return clean;
|
|
506
139
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sealkeeper/schema",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Zod schemas, event taxonomy and signing helpers for SealKeeper.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -64,6 +64,6 @@
|
|
|
64
64
|
"drizzle-orm": "^0.45.3",
|
|
65
65
|
"postgres": "^3.4.9",
|
|
66
66
|
"typescript": "^7.0.2",
|
|
67
|
-
"vitest": "^5.0.
|
|
67
|
+
"vitest": "^5.0.2"
|
|
68
68
|
}
|
|
69
69
|
}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import type { TemplateSpec } from './task-templates.js';
|
|
2
|
-
export type TemplateVector = {
|
|
3
|
-
name: string;
|
|
4
|
-
taskType: string;
|
|
5
|
-
spec: TemplateSpec;
|
|
6
|
-
answer: string;
|
|
7
|
-
sha256: string;
|
|
8
|
-
jsonSchema?: Record<string, unknown>;
|
|
9
|
-
};
|
|
10
|
-
export declare const TEMPLATE_VECTORS: readonly TemplateVector[];
|