@zenera/faker 1.1.22 → 1.1.24

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/README.md CHANGED
@@ -57,8 +57,8 @@ zen faker cache clear # throw it away
57
57
 
58
58
  `zen mock` is the same command under a shorter noun.
59
59
 
60
- A path key with a query string on it — `/x/{id}?action=retry`, which VMware NSX
61
- and others write when they need two `post`s under one path — is read for what
60
+ A path key with a query string on it — `/x/{id}?action=retry`, which some
61
+ vendors write when they need two `post`s under one path — is read for what
62
62
  it means rather than taken verbatim: the path is the part before the `?`, and
63
63
  each pair after it becomes a required query value that tells those operations
64
64
  apart. Call one without it and the 404 says which queries the path answers to.
@@ -75,11 +75,17 @@ for. `GET /users/12324` answering with somebody else's id validates perfectly
75
75
  and is still wrong.
76
76
 
77
77
  If it fails, the diagnostics go back to the model and it tries again, up to
78
- `--attempts`. If it passes, the generator is kept in this machine's shared cache
79
- under `~/.zenera/neo/cache/faker-generator/`, keyed by the operation's shape, and
80
- every later request is just `podman exec python3 gen.py in.json out.json` - no
81
- model, no tokens. The store is the machine's, so the same document served from
82
- another directory costs nothing the second time.
78
+ `--attempts` (five by default). A build that does not come good leaves a
79
+ **report** in `<cache>/builds/` - one markdown file per build, every rejected
80
+ attempt in it with its diagnostics and its whole source, and the `501` that the
81
+ request gets links to it, in the log line and in the body. The next attempt
82
+ overwrites `gen.py`, and a generator that merely hung leaves no traceback, so
83
+ the code is the only evidence there is. If it passes, the generator is kept in
84
+ this machine's shared cache under `~/.zenera/neo/cache/faker-generator/`, keyed
85
+ by the operation's shape, and every later request is just
86
+ `podman exec python3 gen.py in.json out.json` - no model, no tokens. The store
87
+ is the machine's, so the same document served from another directory costs
88
+ nothing the second time.
83
89
 
84
90
  Generators run in a container with **no network**, on an image baked once with
85
91
  `faker`, `exrex`, `jsonschema` and `python-dateutil`.
package/dist/cache.d.ts CHANGED
@@ -27,6 +27,8 @@ export interface CacheEvent {
27
27
  operation: Operation;
28
28
  attempt?: number;
29
29
  diagnostics?: readonly string[];
30
+ /** where the rejected generator was written down, when it was */
31
+ dump?: string;
30
32
  }
31
33
  export interface CacheOptions {
32
34
  box: Box;
@@ -46,6 +48,8 @@ export interface CacheOptions {
46
48
  ephemeral?: boolean;
47
49
  /** keep them somewhere other than the shared store */
48
50
  cacheDir?: string;
51
+ /** where rejected generators are written down. Default `<box root>/builds` */
52
+ dumpDir?: string;
49
53
  onStart?: (e: CacheEvent) => void;
50
54
  onAttempt?: (e: CacheEvent) => void;
51
55
  onReady?: (e: CacheEvent & {
package/dist/cache.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { Cache as Store } from '@zenera/cli/lib';
2
+ import { join } from 'node:path';
2
3
  import { build, BuildFailed, regenerate } from "./generate.js";
4
+ import { saveBuildDump } from "./logger.js";
3
5
  import { GENERATOR_RULES_VERSION } from "./spec.js";
4
6
  // ---------------------------------------------------------------------------
5
7
  // The cache
@@ -24,6 +26,8 @@ const DEFAULT_CONCURRENCY = 4;
24
26
  export class Cache {
25
27
  #opts;
26
28
  #store;
29
+ #dumpDir;
30
+ #records = new Map();
27
31
  #live = new Map();
28
32
  #settled = new Set();
29
33
  #slots;
@@ -32,6 +36,7 @@ export class Cache {
32
36
  constructor(opts) {
33
37
  this.#opts = opts;
34
38
  this.#store = new Store(FAKER_KIND, { dir: opts.cacheDir, mode: 0o600 });
39
+ this.#dumpDir = opts.dumpDir ?? join(opts.box.root, 'builds');
35
40
  this.#slots = Math.max(1, opts.concurrency ?? DEFAULT_CONCURRENCY);
36
41
  }
37
42
  /**
@@ -87,7 +92,7 @@ export class Cache {
87
92
  fault: ctx.fault,
88
93
  stderr: ctx.stderr,
89
94
  examples: ctx.examples,
90
- onAttempt: (attempt, diagnostics) => this.#opts.onAttempt?.({ operation, attempt, diagnostics }),
95
+ onAttempt: (attempt, diagnostics, source, limit) => this.#rejected(operation, attempt, diagnostics, source, limit, true),
91
96
  });
92
97
  if (!ephemeral) {
93
98
  this.#store.put(operation.key, {
@@ -108,11 +113,13 @@ export class Cache {
108
113
  const gen = { key: operation.key, source: built.source, cached: false };
109
114
  this.#settled.add(operation.key);
110
115
  this.#live.set(operation.key, Promise.resolve(gen));
116
+ await this.#finished(operation);
111
117
  return gen;
112
118
  }
113
119
  catch (err) {
114
120
  const error = err instanceof Error ? err : new Error(String(err));
115
- this.#opts.onFail?.({ operation, error });
121
+ const dump = await this.#finished(operation, error);
122
+ this.#opts.onFail?.({ operation, error, dump });
116
123
  throw error;
117
124
  }
118
125
  finally {
@@ -136,7 +143,7 @@ export class Cache {
136
143
  box,
137
144
  checks,
138
145
  attempts: this.#opts.attempts,
139
- onAttempt: (attempt, diagnostics) => this.#opts.onAttempt?.({ operation, attempt, diagnostics }),
146
+ onAttempt: (attempt, diagnostics, source, limit) => this.#rejected(operation, attempt, diagnostics, source, limit, false),
140
147
  });
141
148
  if (!ephemeral) {
142
149
  this.#store.put(operation.key, {
@@ -154,17 +161,78 @@ export class Cache {
154
161
  });
155
162
  }
156
163
  this.#opts.onReady?.({ operation, cached: false, attempts: built.attempts });
164
+ await this.#finished(operation);
157
165
  return { key: operation.key, source: built.source, cached: false };
158
166
  }
159
167
  catch (err) {
160
168
  const error = err instanceof Error ? err : new Error(String(err));
161
- this.#opts.onFail?.({ operation, error });
169
+ const dump = await this.#finished(operation, error);
170
+ this.#opts.onFail?.({ operation, error, dump });
162
171
  throw error;
163
172
  }
164
173
  finally {
165
174
  this.#leave();
166
175
  }
167
176
  }
177
+ /**
178
+ * An attempt the judge threw out. The report is rewritten with the file
179
+ * still in it before the next attempt overwrites `gen.py` — for a timeout
180
+ * that source is the only evidence there will ever be, since a generator
181
+ * that hangs leaves no traceback behind.
182
+ */
183
+ async #rejected(operation, attempt, diagnostics, source, limit, regeneration) {
184
+ let record = this.#records.get(operation.key);
185
+ if (!record || attempt === 1) {
186
+ record = {
187
+ operation,
188
+ started: new Date(),
189
+ limit,
190
+ regeneration,
191
+ attempts: [],
192
+ };
193
+ this.#records.set(operation.key, record);
194
+ }
195
+ record.attempts.push({ attempt, at: new Date(), diagnostics, source });
196
+ await this.#report(record);
197
+ this.#opts.onAttempt?.({ operation, attempt, diagnostics, dump: record.path });
198
+ }
199
+ /** A report that cannot be written is not worth failing a build over. */
200
+ async #report(record, gaveUp = false) {
201
+ try {
202
+ record.path = await saveBuildDump(this.#dumpDir, {
203
+ started: record.started,
204
+ operationId: record.operation.operationId,
205
+ method: record.operation.method,
206
+ path: record.operation.path,
207
+ key: record.operation.key,
208
+ limit: record.limit,
209
+ regeneration: record.regeneration,
210
+ model: this.#opts.model.id,
211
+ attempts: record.attempts,
212
+ gaveUp,
213
+ });
214
+ }
215
+ catch {
216
+ // no report, then — the narration still says what failed
217
+ }
218
+ }
219
+ /**
220
+ * The end of a build, either way. A failure is stamped as such and its
221
+ * report handed to the error, so the request that gets the 501 can say
222
+ * where to read about it.
223
+ */
224
+ async #finished(operation, error) {
225
+ const record = this.#records.get(operation.key);
226
+ this.#records.delete(operation.key);
227
+ if (!record) {
228
+ return undefined;
229
+ }
230
+ if (error instanceof BuildFailed) {
231
+ await this.#report(record, true);
232
+ error.dump = record.path;
233
+ }
234
+ return record.path;
235
+ }
168
236
  /**
169
237
  * A hit is written into the box before it is returned. The container runs
170
238
  * files under its mount and nothing else, and the mount is scratch that any
package/dist/command.js CHANGED
@@ -5,6 +5,7 @@ import { join, relative, resolve } from 'node:path';
5
5
  import { GENERATORS } from "./box.js";
6
6
  import { FAKER_KIND } from "./cache.js";
7
7
  import { reason } from "./generate.js";
8
+ import { formatDumpPath } from "./logger.js";
8
9
  import { listen } from "./server.js";
9
10
  import { chooseModel, MODEL_ENV, open, readSettings, SOURCE_LABELS, writeSettings, } from "./setup.js";
10
11
  import { called } from "./spec.js";
@@ -60,7 +61,7 @@ export const command = {
60
61
  [' --image <ref>', dim('Skip the baked image and use this one.')],
61
62
  [' --cache <dir>', dim("The container's workspace. Default ~/.zenera/neo/faker.")],
62
63
  [' --seed <n>', dim('Answer the same request the same way every time.')],
63
- [' --attempts <n>', dim('Tries per generator before giving up. Default 3.')],
64
+ [' --attempts <n>', dim('Tries per generator before giving up. Default 5.')],
64
65
  [' --concurrency <n>', dim('Generators written at once. Default 4.')],
65
66
  [' --timeout <s>', dim('Seconds one generator may take. Default 30.')],
66
67
  [' --max-body <n>', dim('Largest request body accepted, in bytes.')],
@@ -125,6 +126,7 @@ async function serve(args, ctx) {
125
126
  }
126
127
  note(dim(`generators: ${setup.root}/${GENERATORS}`));
127
128
  note(dim(`request logs: ${requestsDir ?? `${setup.root}/requests`}`));
129
+ note(dim(`failed builds: ${setup.root}/builds`));
128
130
  }
129
131
  // The address is the answer; the rest was narration.
130
132
  write(`http://${host}:${listener.port}`);
@@ -385,7 +387,12 @@ async function start(values, specs, ctx, what) {
385
387
  : note(` ${green('ready')} ${dim(operation.operationId)}`)
386
388
  : undefined,
387
389
  onFail: loud
388
- ? ({ operation, error }) => note(` ${red('gave up')} ${dim(operation.operationId)} ${reason(error)}`)
390
+ ? ({ operation, error, dump }) => {
391
+ note(` ${red('gave up')} ${dim(operation.operationId)} ${reason(error)}`);
392
+ if (dump) {
393
+ note(formatDumpPath('report', dump));
394
+ }
395
+ }
389
396
  : undefined,
390
397
  },
391
398
  });
@@ -10,13 +10,20 @@ export interface BuildOptions {
10
10
  checks: Checks;
11
11
  /** how many times the model may be asked before giving up */
12
12
  attempts?: number;
13
- onAttempt?: (attempt: number, diagnostics: readonly string[]) => void;
13
+ onAttempt?: (attempt: number, diagnostics: readonly string[], source: string, limit: number) => void | Promise<void>;
14
14
  signal?: AbortSignal;
15
15
  }
16
16
  export interface Built {
17
17
  source: string;
18
18
  attempts: number;
19
19
  }
20
+ /**
21
+ * Tries per generator. Each one carries the previous file and what was wrong
22
+ * with it, so the later attempts are the informed ones — a model that has seen
23
+ * its own traceback usually needs two or three goes at a fiddly schema, and the
24
+ * whole loop costs nothing once the file is cached.
25
+ */
26
+ export declare const DEFAULT_ATTEMPTS = 5;
20
27
  export interface RegenerateOptions extends BuildOptions {
21
28
  currentSource: string;
22
29
  failingInput: GeneratorInput;
@@ -27,6 +34,8 @@ export interface RegenerateOptions extends BuildOptions {
27
34
  export declare class BuildFailed extends Error {
28
35
  readonly operation: Operation;
29
36
  readonly diagnostics: readonly string[];
37
+ /** the build report on disk, filled in by whoever was writing one */
38
+ dump?: string;
30
39
  constructor(operation: Operation, diagnostics: readonly string[]);
31
40
  }
32
41
  /**
package/dist/generate.js CHANGED
@@ -3,9 +3,18 @@ import { echoIssues, nextPage, probesFor, walkStart } from "./probe.js";
3
3
  import { instruction, regenerationInstruction, retry, SYSTEM, } from "./prompt.js";
4
4
  import { called } from "./spec.js";
5
5
  import { describeIssues, issues } from "./validate.js";
6
+ /**
7
+ * Tries per generator. Each one carries the previous file and what was wrong
8
+ * with it, so the later attempts are the informed ones — a model that has seen
9
+ * its own traceback usually needs two or three goes at a fiddly schema, and the
10
+ * whole loop costs nothing once the file is cached.
11
+ */
12
+ export const DEFAULT_ATTEMPTS = 5;
6
13
  export class BuildFailed extends Error {
7
14
  operation;
8
15
  diagnostics;
16
+ /** the build report on disk, filled in by whoever was writing one */
17
+ dump;
9
18
  constructor(operation, diagnostics) {
10
19
  super(`could not write a generator for ${operation.method.toUpperCase()} ${called(operation)}`);
11
20
  this.name = 'BuildFailed';
@@ -41,7 +50,7 @@ export function reason(err) {
41
50
  return parts.join(' — ') || String(err);
42
51
  }
43
52
  export async function build(operation, opts) {
44
- const limit = Math.max(1, opts.attempts ?? 3);
53
+ const limit = Math.max(1, opts.attempts ?? DEFAULT_ATTEMPTS);
45
54
  const probes = probesFor(operation);
46
55
  const response = opts.checks.for(operation).response;
47
56
  const messages = [
@@ -66,13 +75,13 @@ export async function build(operation, opts) {
66
75
  return { source, attempts: attempt };
67
76
  }
68
77
  }
69
- opts.onAttempt?.(attempt, last);
78
+ await opts.onAttempt?.(attempt, last, source, limit);
70
79
  messages.push({ role: 'assistant', content: source }, { role: 'user', content: [{ type: 'text', text: retry(last) }] });
71
80
  }
72
81
  throw new BuildFailed(operation, last);
73
82
  }
74
83
  export async function regenerate(operation, opts) {
75
- const limit = Math.max(1, opts.attempts ?? 3);
84
+ const limit = Math.max(1, opts.attempts ?? DEFAULT_ATTEMPTS);
76
85
  const exampleInputs = opts.examples?.map((e) => e.input) ?? [];
77
86
  const probes = [opts.failingInput, ...exampleInputs, ...probesFor(operation)];
78
87
  const response = opts.checks.for(operation).response;
@@ -113,7 +122,7 @@ export async function regenerate(operation, opts) {
113
122
  return { source, attempts: attempt };
114
123
  }
115
124
  }
116
- opts.onAttempt?.(attempt, last);
125
+ await opts.onAttempt?.(attempt, last, source, limit);
117
126
  messages.push({ role: 'assistant', content: source }, { role: 'user', content: [{ type: 'text', text: retry(last) }] });
118
127
  }
119
128
  throw new BuildFailed(operation, last);
package/dist/logger.d.ts CHANGED
@@ -20,6 +20,8 @@ export interface RequestDumpData {
20
20
  errorKind?: ErrorKind;
21
21
  errorMessage?: string;
22
22
  stderr?: string;
23
+ /** the build report, when the answer was "there is no generator" */
24
+ buildDump?: string;
23
25
  cacheStatus?: 'hit' | 'miss' | 'regenerated';
24
26
  regenerated?: boolean;
25
27
  regenAttempts?: number;
@@ -37,6 +39,29 @@ export declare function colorForErrorKind(kind: ErrorKind): (s: string) => strin
37
39
  * Returns the absolute path of the generated markdown file.
38
40
  */
39
41
  export declare function saveRequestDump(dir: string, data: RequestDumpData): Promise<string>;
42
+ export interface BuildAttempt {
43
+ attempt: number;
44
+ at: Date;
45
+ diagnostics: readonly string[];
46
+ source: string;
47
+ }
48
+ export interface BuildDumpData {
49
+ started: Date;
50
+ operationId?: string;
51
+ method: string;
52
+ path: string;
53
+ /** the operation's cache key, which is also its directory in the box */
54
+ key: string;
55
+ limit: number;
56
+ /** rewriting a generator that already existed rather than writing the first */
57
+ regeneration?: boolean;
58
+ model?: string;
59
+ attempts: readonly BuildAttempt[];
60
+ /** the attempts ran out — no generator was produced */
61
+ gaveUp?: boolean;
62
+ }
63
+ export declare function saveBuildDump(dir: string, data: BuildDumpData): Promise<string>;
64
+ export declare function formatDumpPath(label: string, path: string): string;
40
65
  export declare function formatLogLine(data: RequestDumpData, dumpPath: string): string;
41
66
  export declare function formatRegenLimitHit(urlPath: string, limit: number, operationId?: string): string;
42
67
  export declare function formatRegenerating(urlPath: string, attempt: number, limit: number, operationId?: string, reason?: string): string;
package/dist/logger.js CHANGED
@@ -102,6 +102,9 @@ export async function saveRequestDump(dir, data) {
102
102
  if (data.note) {
103
103
  lines.push(`- **Note**: ${data.note}`);
104
104
  }
105
+ if (data.buildDump) {
106
+ lines.push(`- **Build report**: [${data.buildDump}](file://${data.buildDump})`);
107
+ }
105
108
  lines.push('', '---', '', '## Request Info', '');
106
109
  lines.push('### URL & Method', '```http', `${data.method.toUpperCase()} ${data.url}`, '```', '');
107
110
  lines.push('### Path Parameters');
@@ -176,6 +179,38 @@ export async function saveRequestDump(dir, data) {
176
179
  await writeFile(fullPath, lines.join('\n'), 'utf8');
177
180
  return resolve(fullPath);
178
181
  }
182
+ export async function saveBuildDump(dir, data) {
183
+ mkdirSync(dir, { recursive: true });
184
+ const safePath = data.path.replace(/[^a-zA-Z0-9_-]/g, '_').replace(/^_+|_+$/g, '') || 'root';
185
+ const isoPrefix = data.started.toISOString().replace(/[:.]/g, '-');
186
+ const what = data.regeneration ? 'regen' : 'build';
187
+ const fullPath = join(dir, `${isoPrefix}-${what}-${data.method.toUpperCase()}-${safePath}.md`);
188
+ const outcome = data.gaveUp
189
+ ? `gave up after ${data.attempts.length} of ${data.limit}`
190
+ : `attempt ${data.attempts.length} of ${data.limit} rejected`;
191
+ const lines = [
192
+ `# ${data.regeneration ? 'Regeneration' : 'Build'}: ` +
193
+ `${data.method.toUpperCase()} ${data.path}`,
194
+ '',
195
+ `- **Operation ID**: ${data.operationId ? `\`${data.operationId}\`` : '_None_'}`,
196
+ `- **Key**: \`${data.key}\``,
197
+ `- **Started**: ${data.started.toISOString()}`,
198
+ `- **Model**: ${data.model ? `\`${data.model}\`` : '_Unknown_'}`,
199
+ `- **Outcome**: ${outcome}`,
200
+ '',
201
+ ];
202
+ for (const a of data.attempts) {
203
+ const elapsed = Math.max(0, a.at.getTime() - data.started.getTime());
204
+ lines.push('---', '', `## Attempt ${a.attempt} of ${data.limit}`, '', `_Rejected ${a.at.toISOString()}, ${elapsed}ms into the build._`, '', '### Why it was rejected', '', ...(a.diagnostics.length > 0
205
+ ? a.diagnostics.map((d) => (d.trimStart().startsWith('-') ? d : `- ${d}`))
206
+ : ['_No diagnostics._']), '', '### Generator', '', '```python', a.source, '```', '');
207
+ }
208
+ await writeFile(fullPath, lines.join('\n'), 'utf8');
209
+ return resolve(fullPath);
210
+ }
211
+ export function formatDumpPath(label, path) {
212
+ return ` ${dim(`↳ ${label}:`)} ${cyan(`file://${resolve(path)}`)}`;
213
+ }
179
214
  // ---------------------------------------------------------------------------
180
215
  // Log Line Formatter
181
216
  // ---------------------------------------------------------------------------
@@ -206,6 +241,9 @@ export function formatLogLine(data, dumpPath) {
206
241
  const fileUrl = `file://${resolve(dumpPath)}`;
207
242
  lines.push(` ${dim('↳ dump:')} ${cyan(fileUrl)}`);
208
243
  }
244
+ if (data.buildDump) {
245
+ lines.push(formatDumpPath('build', data.buildDump));
246
+ }
209
247
  return lines.join('\n');
210
248
  }
211
249
  // ---------------------------------------------------------------------------
package/dist/probe.js CHANGED
@@ -12,13 +12,25 @@ import { propertyNames } from "./schema.js";
12
12
  // schema allows — a generator that hard-codes the first probe's id passes once
13
13
  // and fails the second, which is the mistake this catches.
14
14
  // ---------------------------------------------------------------------------
15
+ /**
16
+ * How many items a probe asks a paged operation for.
17
+ *
18
+ * Not the schema's maximum. `bounded` below answers an unbounded want with
19
+ * `maximum - 1`, so a `page_size` declared `maximum: 1000` used to make every
20
+ * probe ask for 999 fabricated objects — minutes of Faker inside a container
21
+ * with a 30s budget, reported as "the file took longer than 30s" and blamed on
22
+ * a generator that was doing exactly what it was told. A page is judged on its
23
+ * shape, and three rows show it.
24
+ */
25
+ const PROBE_PAGE_SIZE = 3;
15
26
  export function probesFor(operation) {
27
+ const size = operation.paging?.size;
16
28
  return [0, 1].map((variant) => ({
17
29
  operationId: operation.operationId,
18
30
  method: operation.method,
19
31
  path: operation.path,
20
- pathParams: values(operation.params, 'path', variant),
21
- query: values(operation.params, 'query', variant),
32
+ pathParams: values(operation.params, 'path', variant, size),
33
+ query: values(operation.params, 'query', variant, size),
22
34
  headers: {},
23
35
  body: operation.requestBody
24
36
  ? sample(operation.requestBody.schema, variant, 0, operation.requestBody.schema)
@@ -26,7 +38,7 @@ export function probesFor(operation) {
26
38
  seed: variant === 0 ? 1 : 2,
27
39
  }));
28
40
  }
29
- function values(params, where, variant) {
41
+ function values(params, where, variant, size) {
30
42
  const out = {};
31
43
  for (const p of params) {
32
44
  if (p.in !== where) {
@@ -37,7 +49,10 @@ function values(params, where, variant) {
37
49
  if (!p.required && variant === 1) {
38
50
  continue;
39
51
  }
40
- out[p.name] = sample(p.schema, variant, 0, p.schema);
52
+ out[p.name] =
53
+ p.name === size
54
+ ? bounded(p.schema, PROBE_PAGE_SIZE, true)
55
+ : sample(p.schema, variant, 0, p.schema);
41
56
  }
42
57
  return out;
43
58
  }
package/dist/server.js CHANGED
@@ -94,6 +94,7 @@ async function handle(req, res, opts) {
94
94
  errorKind: meta.errorKind,
95
95
  errorMessage: meta.errorMessage,
96
96
  stderr: meta.stderr,
97
+ buildDump: meta.buildDump,
97
98
  cacheStatus: meta.cacheStatus,
98
99
  regenerated: meta.regenerated,
99
100
  regenAttempts: meta.regenAttempts,
@@ -245,6 +246,7 @@ async function handle(req, res, opts) {
245
246
  error: `no generator for ${operation.operationId}`,
246
247
  detail,
247
248
  diagnostics: err instanceof BuildFailed ? err.diagnostics : undefined,
249
+ report: err instanceof BuildFailed ? err.dump : undefined,
248
250
  };
249
251
  const end = prepareSend(res, status, bodyObj);
250
252
  await finish(status, {
@@ -254,6 +256,7 @@ async function handle(req, res, opts) {
254
256
  requestBody: body,
255
257
  errorKind: 'GENERATOR_BUILD_FAILED',
256
258
  errorMessage: `no generator: ${detail}`,
259
+ buildDump: err instanceof BuildFailed ? err.dump : undefined,
257
260
  responseBody: bodyObj,
258
261
  note: `no generator: ${detail}`,
259
262
  });
package/dist/spec.js CHANGED
@@ -97,7 +97,7 @@ function join(prefix, template) {
97
97
  /** The path as it has to be called — what a person needs to see. */
98
98
  export const called = (op) => op.fixed ? `${op.path}?${new URLSearchParams(op.fixed).toString()}` : op.path;
99
99
  /**
100
- * A path template has no query string, but VMware, Oracle and others write one
100
+ * A path template has no query string, but some vendors write one
101
101
  * anyway — `/x/{id}?action=retry` — because they need two `post`s under one
102
102
  * path and the object model gives them one. Taken verbatim the key matches
103
103
  * nothing, since a request path never contains a `?`. So it is read for what
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenera/faker",
3
- "version": "1.1.22",
3
+ "version": "1.1.24",
4
4
  "description": "Mock HTTP server for swagger/OpenAPI documents, with response bodies generated by a model.",
5
5
  "keywords": [
6
6
  "openapi",
@@ -45,7 +45,7 @@
45
45
  "@apidevtools/swagger-parser": "^12.0.0",
46
46
  "ajv": "^8.17.1",
47
47
  "ajv-formats": "^3.0.1",
48
- "@zenera/cli": "^1.1.22",
49
- "@zenera/neo": "^1.1.22"
48
+ "@zenera/cli": "^1.1.24",
49
+ "@zenera/neo": "^1.1.24"
50
50
  }
51
51
  }