@zenera/faker 1.1.23 → 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 +13 -7
- package/dist/cache.d.ts +4 -0
- package/dist/cache.js +72 -4
- package/dist/command.js +9 -2
- package/dist/generate.d.ts +10 -1
- package/dist/generate.js +13 -4
- package/dist/logger.d.ts +25 -0
- package/dist/logger.js +38 -0
- package/dist/probe.js +19 -4
- package/dist/server.js +3 -0
- package/dist/spec.js +1 -1
- package/package.json +3 -3
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
|
|
61
|
-
|
|
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
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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.#
|
|
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
|
|
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 }) =>
|
|
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
|
});
|
package/dist/generate.d.ts
CHANGED
|
@@ -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 ??
|
|
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 ??
|
|
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] =
|
|
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
|
|
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.
|
|
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.
|
|
49
|
-
"@zenera/neo": "^1.1.
|
|
48
|
+
"@zenera/cli": "^1.1.24",
|
|
49
|
+
"@zenera/neo": "^1.1.24"
|
|
50
50
|
}
|
|
51
51
|
}
|