@sealkeeper/schema 0.5.0 → 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.
@@ -1,24 +1,15 @@
1
- import { utf8Encode } from './base64url.js';
2
1
  /*
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.
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
- * 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.
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,376 +22,53 @@ 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
- 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 = [
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
- 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
- },
33
+ id: 'text_dedupe',
34
+ kind: 'hash',
35
+ input: 'optional',
36
+ category: 'data',
37
+ size: 's',
38
+ difficulty: 1,
253
39
  },
254
40
  {
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
- },
41
+ id: 'line_sort',
42
+ kind: 'hash',
43
+ input: 'optional',
44
+ category: 'data',
45
+ size: 's',
46
+ difficulty: 1,
282
47
  },
283
48
  {
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
- };
49
+ id: 'json_shape',
50
+ kind: 'schema',
51
+ input: 'none',
52
+ category: 'data',
53
+ size: 's',
54
+ difficulty: 2,
342
55
  },
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
- };
56
+ {
57
+ id: 'summarise',
58
+ kind: 'counterparty',
59
+ input: 'required',
60
+ category: 'writing',
61
+ size: 's',
62
+ difficulty: 3,
369
63
  },
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
- };
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
  ];
405
73
  /*
406
74
  * One in this many of the candidates the server's candidate run makes is a
@@ -413,102 +81,59 @@ export const CONFIRM_CANDIDATE_ONE_IN = 4;
413
81
  export function taskTemplateById(id) {
414
82
  return TASK_TEMPLATES.find((t) => t.id === id);
415
83
  }
416
- const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
417
- // The spec's three strings, or bad_input.
418
- function specStrings(spec) {
419
- if (!isRecord(spec) ||
420
- typeof spec.instruction !== 'string' ||
421
- typeof spec.input !== 'string' ||
422
- typeof spec.output !== 'string') {
423
- throw new TemplateSolveError('bad_input', 'the spec needs instruction, input and output as strings');
424
- }
425
- return {
426
- instruction: spec.instruction,
427
- input: spec.input,
428
- output: spec.output,
429
- };
430
- }
431
- function requireRule(spec, rule, taskType) {
432
- if (spec.instruction !== rule.instruction || spec.output !== rule.output) {
433
- throw new TemplateSolveError('unknown_rule', `the spec is not worded as the ${taskType} template words it`);
434
- }
435
- }
436
- // The lines of a posted input. Only an input make could have written is
437
- // solved, so the answer never rests on a reading of blank lines or carriage
438
- // returns that the poster's CLI never produces. Checked as posted, never
439
- // cleaned again, since make drops only one leading byte order mark and a
440
- // second one is part of the first line.
441
- function specLines(input) {
442
- const refuse = (why) => {
443
- throw new TemplateSolveError('bad_input', why);
444
- };
445
- if (input.trim() === '')
446
- refuse('the input is empty');
447
- if (input.length > TEMPLATE_MAX_INPUT_CHARS) {
448
- refuse(`the input is longer than ${TEMPLATE_MAX_INPUT_CHARS} characters`);
449
- }
450
- if (input.includes('\r'))
451
- refuse('the input has a carriage return');
452
- const lines = input.split('\n');
453
- if (lines.includes('')) {
454
- refuse('the input has an empty line or a line feed at the end');
455
- }
456
- return lines;
457
- }
458
- function solveShape(spec) {
459
- if (spec.instruction !== SHAPE_INSTRUCTION) {
460
- throw new TemplateSolveError('unknown_rule', 'the spec is not worded as the json_shape template words it');
461
- }
462
- const shape = SHAPES.find((s) => shapeOutput(s) === spec.output);
463
- if (shape === undefined) {
464
- throw new TemplateSolveError('unknown_rule', 'the output names fields no json_shape shape has');
465
- }
466
- const value = shape.parse(spec.input);
467
- // The sentence written again from the value must be the input, so a
468
- // number with a leading zero or a sentence that parses two ways is
469
- // refused rather than guessed at.
470
- if (value === undefined || shape.text(value) !== spec.input) {
471
- throw new TemplateSolveError('bad_input', 'the sentence is not one the json_shape template writes');
472
- }
473
- return JSON.stringify(value);
474
- }
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;
475
90
  /*
476
- * The answer that passes a template task, from its task type and spec. For
477
- * a hash template it is the exact text whose sha256 the task carries, UTF-8,
478
- * with its one final line feed. For a schema template it is the JSON text
479
- * of the one object the schema accepts, with the keys in field order. A
480
- * poster hashes this text and the taker submits it.
481
- *
482
- * Throws TemplateSolveError for a task type outside the template set, a
483
- * counterparty template, a spec not worded as the template words it, or an
484
- * input the template would never have made. It never guesses.
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.
485
100
  *
486
- * For the taker (TAKER-3). It is pure and has no side effects, so it is
487
- * safe to call before claiming. A caller must still compare the answer's
488
- * sha256 with the stored digest, or check the answer against the stored
489
- * jsonSchema, before it claims, since the origin and the digest are set by
490
- * the poster and a spec in the template's words can carry any digest. Treat
491
- * 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.
492
105
  */
493
- export function solveTemplate(taskType, spec) {
494
- const template = taskTemplateById(taskType);
495
- if (template === undefined) {
496
- throw new TemplateSolveError('unknown_template', `no template ${taskType}`);
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;
497
113
  }
498
- if (template.kind === 'counterparty') {
499
- throw new TemplateSolveError('no_server_check', `${taskType} is judged by its poster and has no one right answer`);
114
+ if (input === undefined) {
115
+ if (template.input === 'required') {
116
+ throw new TemplateInputError(`this template needs ${NEEDS[id]}`);
117
+ }
118
+ return undefined;
500
119
  }
501
- const s = specStrings(spec);
502
- switch (template.id) {
503
- case 'text_dedupe':
504
- requireRule(s, DEDUPE_RULE, taskType);
505
- return joinLines(dedupe(specLines(s.input)));
506
- case 'line_sort':
507
- requireRule(s, SORT_RULE, taskType);
508
- return joinLines(sortLines(specLines(s.input)));
509
- case 'json_shape':
510
- return solveShape(s);
511
- default:
512
- throw new TemplateSolveError('unknown_template', `no solver for ${taskType}`);
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`);
513
128
  }
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');
131
+ }
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`);
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;
514
139
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sealkeeper/schema",
3
- "version": "0.5.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",
@@ -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[];