@zenera/gym 1.1.40 → 1.1.42

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 (51) hide show
  1. package/README.md +169 -104
  2. package/dist/agents.d.ts +25 -0
  3. package/dist/agents.js +52 -0
  4. package/dist/bindings.d.ts +25 -0
  5. package/dist/bindings.js +81 -0
  6. package/dist/box.d.ts +39 -36
  7. package/dist/box.js +183 -217
  8. package/dist/cache.d.ts +24 -14
  9. package/dist/cache.js +102 -66
  10. package/dist/catalog.d.ts +83 -0
  11. package/dist/catalog.js +293 -0
  12. package/dist/command.d.ts +1 -0
  13. package/dist/command.js +235 -68
  14. package/dist/fake.d.ts +4 -0
  15. package/dist/fake.js +142 -0
  16. package/dist/generate.d.ts +28 -24
  17. package/dist/generate.js +417 -120
  18. package/dist/inputs.d.ts +12 -0
  19. package/dist/inputs.js +132 -0
  20. package/dist/knowledge.d.ts +50 -0
  21. package/dist/knowledge.js +199 -0
  22. package/dist/logger.d.ts +10 -8
  23. package/dist/logger.js +4 -3
  24. package/dist/paging.d.ts +2 -2
  25. package/dist/paging.js +2 -2
  26. package/dist/probe.d.ts +5 -5
  27. package/dist/probe.js +15 -15
  28. package/dist/prompt.d.ts +31 -18
  29. package/dist/prompt.js +160 -146
  30. package/dist/quiet.d.ts +2 -0
  31. package/dist/quiet.js +7 -0
  32. package/dist/router.js +5 -1
  33. package/dist/runtime.d.ts +160 -0
  34. package/dist/runtime.js +229 -0
  35. package/dist/schema.js +1 -2
  36. package/dist/server.js +98 -57
  37. package/dist/setup.d.ts +47 -6
  38. package/dist/setup.js +157 -21
  39. package/dist/spec.d.ts +4 -3
  40. package/dist/spec.js +73 -22
  41. package/dist/store.d.ts +78 -0
  42. package/dist/store.js +253 -0
  43. package/dist/synth.d.ts +42 -0
  44. package/dist/synth.js +589 -0
  45. package/dist/worker.d.ts +41 -0
  46. package/dist/worker.js +99 -0
  47. package/package.json +5 -3
  48. package/dist/envelope.d.ts +0 -13
  49. package/dist/envelope.js +0 -9
  50. package/dist/image.d.ts +0 -25
  51. package/dist/image.js +0 -81
package/dist/synth.js ADDED
@@ -0,0 +1,589 @@
1
+ import { tool } from '@zenera/neo';
2
+ import { runAgent } from "./agents.js";
3
+ import { sample } from "./fake.js";
4
+ import { ACCOUNTS, collectionFor, matches, OWNER, OWNER_TYPE, populatedKey, same, } from "./runtime.js";
5
+ import { leaseOwner, WorldStore } from "./store.js";
6
+ // ---------------------------------------------------------------------------
7
+ // The world agent
8
+ //
9
+ // Handlers know where content lives; this is where it comes from. A handler
10
+ // that finds the world empty — no messages yet, no event with that id — asks
11
+ // for records, and an agent writes them: reading the story, reading what the
12
+ // world already holds so the same people keep the same addresses across APIs,
13
+ // and saving records that are checked against the schema before anything can
14
+ // read them. What it saves is permanent, so the second request sees the first
15
+ // request's world.
16
+ //
17
+ // Judging a handler asks for content too, and gets `fake.ts` instead: a probe
18
+ // is about mechanics, and an agent call per probe would make every build slow
19
+ // and every build cost.
20
+ // ---------------------------------------------------------------------------
21
+ /**
22
+ * Accounts are the one collection no document declares: who `me` is, and who
23
+ * everyone else is, shared by every API in the world. So their shape is ours.
24
+ */
25
+ const ACCOUNT_SCHEMA = {
26
+ type: 'object',
27
+ required: ['id', 'email', 'displayName'],
28
+ properties: {
29
+ id: {
30
+ type: 'string',
31
+ description: `"${OWNER}" for the account the API is called as; otherwise the email`,
32
+ },
33
+ email: { type: 'string', format: 'email' },
34
+ displayName: { type: 'string' },
35
+ timeZone: { type: 'string', description: 'IANA zone, e.g. Europe/Berlin' },
36
+ },
37
+ };
38
+ const ACCOUNT_DECLARATION = `interface Account {
39
+ /** "${OWNER}" for the account the API is called as; otherwise the email */
40
+ id: string;
41
+ email: string;
42
+ displayName: string;
43
+ /** IANA zone, e.g. Europe/Berlin */
44
+ timeZone?: string;
45
+ }`;
46
+ const MAX_RECORDS = 50;
47
+ /** Longer than any world-agent run; a crashed holder's lease lapses at once. */
48
+ const FILL_LEASE_MS = 10 * 60_000;
49
+ const LEASE_POLL_MS = 250;
50
+ const DEFAULT_COUNT = 5;
51
+ const MAX_TOOL_CALLS = 14;
52
+ /** What one read of a collection may put into the prompt. */
53
+ const READ_CHARS = 24_000;
54
+ export class World {
55
+ store;
56
+ /** what the agents know: the story, and the docs with the tools that read them */
57
+ knowledge;
58
+ #opts;
59
+ #stores = new Map();
60
+ #locks = new Map();
61
+ #owner = leaseOwner();
62
+ constructor(opts) {
63
+ this.#opts = opts;
64
+ this.store = opts.store;
65
+ this.knowledge = opts.knowledge;
66
+ }
67
+ /** Given to the box: every request a handler makes for content lands here. */
68
+ synthesize = (request, context) => {
69
+ const req = normalizeRequest(request);
70
+ const store = this.#storeFor(context.db);
71
+ if (context.probe) {
72
+ return Promise.resolve(this.#stub(store, req, context.input.seed));
73
+ }
74
+ // One agent per collection at a time: two requests that both found the
75
+ // inbox empty must not both fill it.
76
+ const key = `${context.db}\u0000${req.collection}`;
77
+ const before = this.#locks.get(key) ?? Promise.resolve();
78
+ const mine = before
79
+ .catch(() => undefined)
80
+ .then(() => this.#make(store, req, context.input));
81
+ this.#locks.set(key, mine);
82
+ void mine.finally(() => {
83
+ if (this.#locks.get(key) === mine) {
84
+ this.#locks.delete(key);
85
+ }
86
+ });
87
+ return mine;
88
+ };
89
+ /** Forgets a world that was only borrowed, such as a judging snapshot. */
90
+ release(db) {
91
+ const store = this.#stores.get(db);
92
+ if (store && store !== this.store) {
93
+ store.close();
94
+ }
95
+ this.#stores.delete(db);
96
+ }
97
+ close() {
98
+ for (const db of [...this.#stores.keys()]) {
99
+ this.release(db);
100
+ }
101
+ }
102
+ // -----------------------------------------------------------------------
103
+ #storeFor(db) {
104
+ if (db === this.store.path) {
105
+ return this.store;
106
+ }
107
+ let store = this.#stores.get(db);
108
+ if (!store) {
109
+ store = new WorldStore(db);
110
+ this.#stores.set(db, store);
111
+ }
112
+ return store;
113
+ }
114
+ /**
115
+ * One filler per collection across every process on this world. The lock
116
+ * above serialises this process; the lease serialises the others, which
117
+ * wait and then find the records made for them.
118
+ */
119
+ async #make(store, req, input) {
120
+ const ready = () => this.#ready(store, req);
121
+ const already = ready();
122
+ if (already) {
123
+ return already;
124
+ }
125
+ const lease = `fill:${req.collection}:${JSON.stringify(req.ids?.length ? req.ids.map(String).sort() : (req.where ?? {}))}`;
126
+ while (!store.claim(lease, this.#owner, FILL_LEASE_MS)) {
127
+ await new Promise((settle) => setTimeout(settle, LEASE_POLL_MS));
128
+ const made = ready();
129
+ if (made) {
130
+ return made;
131
+ }
132
+ }
133
+ try {
134
+ return ready() ?? (await this.#fill(store, req, input));
135
+ }
136
+ finally {
137
+ store.release(lease, this.#owner);
138
+ }
139
+ }
140
+ /** What is wanted, if the world already has it. */
141
+ #ready(store, req) {
142
+ const col = store.collection(req.collection);
143
+ if (req.ids?.length) {
144
+ return req.ids.every((id) => col.has(id))
145
+ ? req.ids.map((id) => col.get(id))
146
+ : undefined;
147
+ }
148
+ if (req.min !== undefined) {
149
+ const have = col.all().filter((r) => matches(r, req.where));
150
+ const populated = store.getMeta(populatedKey(req.collection, req.where));
151
+ if (have.length >= req.min && (!req.populate || populated)) {
152
+ return have;
153
+ }
154
+ }
155
+ return undefined;
156
+ }
157
+ async #fill(store, req, input) {
158
+ const key = populatedKey(req.collection, req.where);
159
+ const started = Date.now();
160
+ const saved = new Map();
161
+ const dependents = new Map();
162
+ const wanted = req.ids?.length ?? Math.min(req.count ?? DEFAULT_COUNT, MAX_RECORDS);
163
+ // A first fill makes what the story says exists, however many that is:
164
+ // the count is only a suggestion, and what is required is what the
165
+ // handler cannot answer without.
166
+ const required = req.ids?.length ?? (req.populate ? Math.max(req.min ?? 1, 1) : wanted);
167
+ const enough = () => req.ids?.length ? req.ids.every((id) => saved.has(String(id))) : saved.size >= required;
168
+ // Stopped as soon as it is provably done — except a first fill, whose
169
+ // end only the agent knows.
170
+ const done = req.populate && !req.ids?.length ? undefined : enough;
171
+ let report;
172
+ let failure;
173
+ try {
174
+ const run = await runAgent({
175
+ model: this.#opts.model,
176
+ name: 'world',
177
+ label: req.collection,
178
+ instructions: this.#instructions(),
179
+ tools: [
180
+ ...this.#opts.knowledge.tools,
181
+ ...this.#tools(store, req, saved, dependents),
182
+ ],
183
+ input: await this.#brief(store, req, input, wanted),
184
+ maxToolCalls: MAX_TOOL_CALLS,
185
+ done,
186
+ reportDir: this.#opts.reportDir,
187
+ });
188
+ report = run.report;
189
+ if (!enough()) {
190
+ failure = `the world agent saved ${saved.size} of ${required} records`;
191
+ }
192
+ }
193
+ catch (err) {
194
+ failure = `the world agent failed: ${err instanceof Error ? err.message : String(err)}`;
195
+ }
196
+ let made = [...saved.values()];
197
+ if (!req.ids?.length) {
198
+ store.setMeta(key, true);
199
+ }
200
+ if (failure) {
201
+ // Better a plausible-shaped answer than a failed request: the rest
202
+ // is sampled from the schema, and the narration says so.
203
+ made = [
204
+ ...made,
205
+ ...this.#stub(store, { ...req, ids: missing(req, saved), count: required - saved.size }, input.seed),
206
+ ];
207
+ }
208
+ const dangling = this.#dangling(store, req, [...saved.values()]);
209
+ this.#opts.onMade?.({
210
+ collection: req.collection,
211
+ made: made.length,
212
+ fallback: failure,
213
+ ...(dependents.size > 0 ? { dependents: dependents.size } : {}),
214
+ ...(dangling.length > 0 ? { dangling } : {}),
215
+ report,
216
+ durationMs: Date.now() - started,
217
+ });
218
+ return made;
219
+ }
220
+ #stub(store, req, seed) {
221
+ const idField = req.idField ?? 'id';
222
+ if (!req.ids?.length) {
223
+ store.setMeta(populatedKey(req.collection, req.where), true);
224
+ }
225
+ const schema = req.type
226
+ ? this.#opts.catalog.types[req.type]
227
+ : req.collection === ACCOUNTS
228
+ ? ACCOUNT_SCHEMA
229
+ : undefined;
230
+ const n = req.ids?.length ?? Math.max(0, Math.min(req.count ?? 3, MAX_RECORDS));
231
+ const col = store.collection(req.collection);
232
+ const out = [];
233
+ for (let i = 0; i < n; i++) {
234
+ const made = schema ? sample(schema, this.#opts.catalog.types, seed * 31 + i + 1) : {};
235
+ const record = typeof made === 'object' && made !== null && !Array.isArray(made)
236
+ ? made
237
+ : {};
238
+ Object.assign(record, req.where ?? {});
239
+ const want = req.ids?.[i];
240
+ // A path segment is text; a schema that calls the id a number gets a number.
241
+ record[idField] =
242
+ want !== undefined
243
+ ? typeof record[idField] === 'number' && /^-?\d+$/.test(String(want))
244
+ ? Number(want)
245
+ : want
246
+ : (record[idField] ?? `${req.collection}-${seed}-${col.count() + i + 1}`);
247
+ col.put(String(record[idField]), record);
248
+ out.push(record);
249
+ }
250
+ return out;
251
+ }
252
+ #instructions() {
253
+ return [
254
+ 'You populate the world behind a simulated API. A request handler needs records',
255
+ 'that do not exist yet; you write them and save them with save_records.',
256
+ '',
257
+ this.#opts.knowledge.story,
258
+ '',
259
+ ...(this.#opts.knowledge.docs
260
+ ? [
261
+ 'REFERENCE DOCUMENTS. The world also has documentation: a knowledge base,',
262
+ 'guides, runbooks. The brief quotes the passages most likely to matter;',
263
+ 'search_docs, grep_docs and read_docs find more. Take realistic values',
264
+ 'from them — names, codes, settings, valid ranges, how things relate —',
265
+ 'and where a record would cite or quote a document, cite or quote it',
266
+ 'truly. The story wins where the two disagree.',
267
+ '',
268
+ ]
269
+ : []),
270
+ 'RULES',
271
+ '- Agree with the story. Use its people, names, addresses, dates, projects and',
272
+ ' themes. Where it is silent, invent what fits it.',
273
+ '- Respect what the story says has NOT happened: a meeting it calls unscheduled',
274
+ ' is not on any calendar, a reply it says was not sent is not in the mailbox.',
275
+ ' Those are left for the user of the API to do.',
276
+ '- Agree with the world. Look at what other collections already hold',
277
+ ' (list_collections, read_collection) and reuse the same people, ids and',
278
+ ' facts: the world is shared by several APIs and must read as one place.',
279
+ '- Never point at what does not exist. A field that names another entity — an',
280
+ ' id, a path, a parent, an owner — names one the world holds: reuse it, or make',
281
+ ' it in this same run with save_records({ type, records }) and point at that.',
282
+ ' Records you make that way are saved in their own collections.',
283
+ '- Every record must satisfy the type you are given. Required fields always;',
284
+ ' optional ones where they make the record more realistic.',
285
+ '- Write content a person would recognise: real-sounding subjects, bodies,',
286
+ ' titles, descriptions; consistent timestamps; ids in the format the schema',
287
+ ' asks for.',
288
+ '- Save everything in one save_records call when you can. If it reports',
289
+ ' errors, fix those records and save them again. Do not save duplicates.',
290
+ '- Stop as soon as the request is satisfied.',
291
+ ].join('\n');
292
+ }
293
+ async #brief(store, req, input, wanted) {
294
+ const catalog = this.#opts.catalog;
295
+ const typed = req.type && catalog.hasType(req.type);
296
+ const lines = [
297
+ `Make records for the collection \`${req.collection}\`, keyed by \`${req.idField}\`.`,
298
+ req.ids?.length
299
+ ? `They must have exactly these ${req.idField} values: ${JSON.stringify(req.ids)}.`
300
+ : req.populate
301
+ ? `Make the records the story says exist; where it does not say, about ${wanted}.`
302
+ : `Make ${wanted} new records.`,
303
+ ];
304
+ if (req.populate) {
305
+ lines.push('This is the first time anyone reads this collection: fill it as the story has it', 'today — a realistic, varied population — not only what this one request asks for.', "Records already in it were written by the API's users; keep them and add around them.");
306
+ }
307
+ if (req.where && Object.keys(req.where).length > 0) {
308
+ lines.push(`Every record must carry these values: ${JSON.stringify(req.where)}.`);
309
+ }
310
+ if (req.hint) {
311
+ lines.push(`What they are for: ${req.hint}`);
312
+ }
313
+ lines.push('', `The request that needs them: ${input.method.toUpperCase()} ${input.path}`, ` path ${JSON.stringify(input.pathParams)} query ${JSON.stringify(input.query)}` +
314
+ (input.body !== undefined
315
+ ? ` body ${clip(JSON.stringify(input.body), 1500)}`
316
+ : ''), `Today is ${new Date().toISOString().slice(0, 10)}.`, '');
317
+ if (typed) {
318
+ const { code, omitted } = catalog.declare([req.type], 16_000);
319
+ lines.push(`Each record is a \`${req.type}\`:`, code.trimEnd());
320
+ if (omitted.length > 0) {
321
+ lines.push(`(also referenced, ask describe_types: ${omitted.join(', ')})`);
322
+ }
323
+ }
324
+ else if (req.collection === ACCOUNTS) {
325
+ lines.push('Each record is an account. The one with id "' +
326
+ OWNER +
327
+ '" is the person the API is', 'called as — the owner of the mailbox, calendar and data in the story.', ACCOUNT_DECLARATION);
328
+ }
329
+ else {
330
+ lines.push('No schema type was named: make records shaped like the response the request expects.');
331
+ }
332
+ const references = req.type ? catalog.references(req.type) : [];
333
+ if (references.length > 0) {
334
+ lines.push('', 'REFERENCES — fields that name other entities. Each must name one the world holds:', ...references.map((r) => ` ${r.name}: ${r.signature}${r.doc ? ` — ${r.doc}` : ''}`));
335
+ }
336
+ const placed = Object.entries(this.#opts.bindings.all()).filter(([type]) => type !== OWNER_TYPE && type !== req.type);
337
+ if (placed.length > 0) {
338
+ lines.push('', 'OTHER TYPES IN THE WORLD (save one with save_records({ type, records }))', ...placed.map(([type, b]) => ` ${type} → ${b.collection}, keyed by ${b.idField}: ${store.collection(b.collection).count()} records`));
339
+ }
340
+ const passages = await this.#opts.knowledge.recall([req.type, req.collection, req.hint, `${input.method.toUpperCase()} ${input.path}`]
341
+ .filter(Boolean)
342
+ .join(' — '));
343
+ if (passages) {
344
+ lines.push('', 'FROM THE REFERENCE DOCUMENTS', passages.trimEnd());
345
+ }
346
+ const world = store.collections();
347
+ lines.push('', 'THE WORLD SO FAR', ...(world.length > 0
348
+ ? world.map((c) => `- ${c.name}: ${c.count} records (e.g. ${c.sampleIds.join(', ')})`)
349
+ : ['- (empty)']));
350
+ return lines.join('\n');
351
+ }
352
+ #tools(store, req, saved, dependents) {
353
+ const catalog = this.#opts.catalog;
354
+ const validate = req.type
355
+ ? catalog.validator(req.type)
356
+ : req.collection === ACCOUNTS
357
+ ? catalog.compile(ACCOUNT_SCHEMA)
358
+ : undefined;
359
+ const list = tool({
360
+ name: 'list_collections',
361
+ description: 'Every collection in the world, with how many records it holds.',
362
+ parameters: { type: 'object', properties: {}, additionalProperties: false },
363
+ execute: () => ({ collections: store.collections() }),
364
+ });
365
+ const read = tool({
366
+ name: 'read_collection',
367
+ description: 'Records already in a collection, oldest first. Read related collections to ' +
368
+ 'reuse their people, ids and facts.',
369
+ parameters: {
370
+ type: 'object',
371
+ properties: {
372
+ collection: { type: 'string' },
373
+ limit: { type: 'integer', description: 'Default 10.' },
374
+ where: { type: 'object', description: 'Only records with these values.' },
375
+ },
376
+ required: ['collection'],
377
+ additionalProperties: false,
378
+ },
379
+ execute: ({ collection, limit, where }) => {
380
+ const all = store
381
+ .collection(collection)
382
+ .all()
383
+ .filter((r) => matches(r, where));
384
+ const records = all.slice(0, Math.min(limit ?? 10, 50));
385
+ return {
386
+ total: all.length,
387
+ records: JSON.parse(clip(JSON.stringify(records), READ_CHARS, true)),
388
+ };
389
+ },
390
+ });
391
+ const describe = tool({
392
+ name: 'describe_types',
393
+ description: 'Schema types by name, as TypeScript, with everything they refer to.',
394
+ parameters: {
395
+ type: 'object',
396
+ properties: { names: { type: 'array', items: { type: 'string' } } },
397
+ required: ['names'],
398
+ additionalProperties: false,
399
+ },
400
+ execute: ({ names }) => {
401
+ const { code, omitted } = catalog.declare(names, 30_000);
402
+ return code
403
+ ? { typescript: code, omitted }
404
+ : { error: `no such type: ${names.join(', ')}` };
405
+ },
406
+ });
407
+ const save = tool({
408
+ name: 'save_records',
409
+ description: `Validates records and saves the valid ones — to \`${req.collection}\` by default, ` +
410
+ 'or, with `type`, to the collection of that type: how the entities these records ' +
411
+ 'refer to are made. Reports every invalid record with what is wrong, so it can be ' +
412
+ 'fixed and saved again.',
413
+ parameters: {
414
+ type: 'object',
415
+ properties: {
416
+ records: { type: 'array', items: { type: 'object' } },
417
+ type: {
418
+ type: 'string',
419
+ description: 'Save records of another schema type, e.g. one these records refer to.',
420
+ },
421
+ },
422
+ required: ['records'],
423
+ additionalProperties: false,
424
+ },
425
+ execute: ({ records, type }) => {
426
+ const other = type && type !== req.type ? this.#placeOf(type) : undefined;
427
+ if (other && 'error' in other) {
428
+ return other;
429
+ }
430
+ const target = other ?? {
431
+ collection: req.collection,
432
+ idField: req.idField,
433
+ request: req,
434
+ validate,
435
+ };
436
+ const col = store.collection(target.collection);
437
+ const errors = [];
438
+ let ok = 0;
439
+ (Array.isArray(records) ? records : []).forEach((record, index) => {
440
+ const problems = this.#problems(record, target.request, target.validate);
441
+ if (problems.length > 0) {
442
+ errors.push({ index, problems });
443
+ return;
444
+ }
445
+ const id = String(record[target.idField]);
446
+ col.put(id, record);
447
+ if (other) {
448
+ dependents.set(`${target.collection}/${id}`, record);
449
+ }
450
+ else {
451
+ saved.set(id, record);
452
+ }
453
+ ok++;
454
+ });
455
+ if (other) {
456
+ return { saved: ok, errors, collection: target.collection };
457
+ }
458
+ const left = missing(req, saved);
459
+ return {
460
+ saved: ok,
461
+ errors,
462
+ ...(left?.length ? { stillMissing: left } : {}),
463
+ ...(!req.ids?.length
464
+ ? req.populate
465
+ ? { savedSoFar: saved.size }
466
+ : {
467
+ stillNeeded: Math.max(0, (req.count ?? DEFAULT_COUNT) - saved.size),
468
+ }
469
+ : {}),
470
+ };
471
+ },
472
+ });
473
+ return [list, read, describe, save];
474
+ }
475
+ /**
476
+ * Where records of another type go, placing the type if nobody has: the
477
+ * world agent can be first to need a transport zone, before any handler has
478
+ * served one.
479
+ */
480
+ #placeOf(type) {
481
+ const catalog = this.#opts.catalog;
482
+ if (!catalog.hasType(type)) {
483
+ return {
484
+ error: `no type called ${type}`,
485
+ hint: 'describe_types shows the ones there are',
486
+ };
487
+ }
488
+ const binding = this.#opts.bindings.of(type) ?? {
489
+ collection: collectionFor(type),
490
+ idField: catalog.idField(type),
491
+ };
492
+ const problem = this.#opts.bindings.bind(type, binding);
493
+ if (problem) {
494
+ return { error: problem };
495
+ }
496
+ return {
497
+ collection: binding.collection,
498
+ idField: binding.idField,
499
+ request: { collection: binding.collection, type, idField: binding.idField },
500
+ validate: catalog.validator(type),
501
+ };
502
+ }
503
+ /**
504
+ * References in the records just made that point at nothing. Judged loosely:
505
+ * a value counts as found if any collection holds a record under it, or
506
+ * under its last path segment — `/infra/tier-1s/t1-app` names `t1-app`.
507
+ * Reported, never repaired: a guess at the missing entity would be worse.
508
+ */
509
+ #dangling(store, req, made) {
510
+ const catalog = this.#opts.catalog;
511
+ const references = (req.type ? catalog.references(req.type) : []).filter((r) => catalog.referent(r.name));
512
+ if (references.length === 0 || made.length === 0) {
513
+ return [];
514
+ }
515
+ const collections = store.collections().map((c) => store.collection(c.name));
516
+ const exists = (value) => {
517
+ const tail = value.split('/').filter(Boolean).at(-1) ?? value;
518
+ return collections.some((c) => c.has(value) || c.has(tail));
519
+ };
520
+ const out = new Set();
521
+ for (const record of made) {
522
+ for (const { name } of references) {
523
+ const value = record[name];
524
+ const values = Array.isArray(value) ? value : [value];
525
+ for (const v of values) {
526
+ if (typeof v === 'string' && v && !exists(v)) {
527
+ out.add(`${name}=${v}`);
528
+ }
529
+ }
530
+ }
531
+ }
532
+ return [...out];
533
+ }
534
+ #problems(record, req, validate) {
535
+ if (typeof record !== 'object' || record === null || Array.isArray(record)) {
536
+ return ['is not an object'];
537
+ }
538
+ const r = record;
539
+ const out = [];
540
+ const id = r[req.idField];
541
+ if (typeof id !== 'string' && typeof id !== 'number') {
542
+ out.push(`\`${req.idField}\` is missing`);
543
+ }
544
+ else if (req.ids?.length && !req.ids.some((want) => String(want) === String(id))) {
545
+ out.push(`\`${req.idField}\` ${JSON.stringify(id)} is not one of the ids asked for`);
546
+ }
547
+ for (const [k, v] of Object.entries(req.where ?? {})) {
548
+ if (!same(r[k], v)) {
549
+ out.push(`\`${k}\` must be ${JSON.stringify(v)}`);
550
+ }
551
+ }
552
+ if (validate && !validate(r)) {
553
+ for (const e of validate.errors ?? []) {
554
+ // An id the client asked for wins over the id format: it is the
555
+ // name the record will be looked up by.
556
+ if (req.ids?.length && e.instancePath === `/${req.idField}`) {
557
+ continue;
558
+ }
559
+ out.push(`${e.instancePath || '(root)'} ${e.message ?? 'is invalid'}`);
560
+ }
561
+ }
562
+ return out;
563
+ }
564
+ }
565
+ function normalizeRequest(request) {
566
+ if (!request || typeof request.collection !== 'string' || !request.collection.trim()) {
567
+ throw new Error('synthesize needs a `collection` name');
568
+ }
569
+ return {
570
+ ...request,
571
+ collection: request.collection.trim(),
572
+ // The owner's type is gym's own, not a document's: accounts are shaped below.
573
+ type: request.type === OWNER_TYPE ? undefined : request.type,
574
+ idField: request.idField ?? 'id',
575
+ ids: request.ids?.slice(0, MAX_RECORDS),
576
+ };
577
+ }
578
+ function missing(req, saved) {
579
+ return req.ids?.filter((id) => !saved.has(String(id)));
580
+ }
581
+ function clip(text, max, json = false) {
582
+ if (text.length <= max) {
583
+ return text;
584
+ }
585
+ return json
586
+ ? JSON.stringify({ truncated: true, preview: text.slice(0, max) })
587
+ : `${text.slice(0, max)}…`;
588
+ }
589
+ //# sourceMappingURL=synth.js.map
@@ -0,0 +1,41 @@
1
+ import { type Bindings, type HandlerRequest, type SynthesisRequest, type Use } from './runtime.ts';
2
+ export type ToWorker = {
3
+ type: 'run';
4
+ id: number;
5
+ file: string;
6
+ version: string;
7
+ db: string;
8
+ input: HandlerRequest;
9
+ bindings: Bindings;
10
+ } | {
11
+ type: 'synthesized';
12
+ id: number;
13
+ call: number;
14
+ records?: unknown[];
15
+ error?: string;
16
+ } | {
17
+ type: 'close';
18
+ db: string;
19
+ };
20
+ export type FromWorker = {
21
+ type: 'synthesize';
22
+ id: number;
23
+ call: number;
24
+ db: string;
25
+ request: SynthesisRequest;
26
+ } | {
27
+ type: 'done';
28
+ id: number;
29
+ ok: true;
30
+ value: unknown;
31
+ status?: number;
32
+ used: Use[];
33
+ } | {
34
+ type: 'done';
35
+ id: number;
36
+ ok: false;
37
+ fault: string;
38
+ stderr?: string;
39
+ used: Use[];
40
+ };
41
+ //# sourceMappingURL=worker.d.ts.map