@zenera/gym 1.1.39 → 1.1.41
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 +169 -104
- package/dist/agents.d.ts +25 -0
- package/dist/agents.js +52 -0
- package/dist/bindings.d.ts +25 -0
- package/dist/bindings.js +81 -0
- package/dist/box.d.ts +39 -36
- package/dist/box.js +183 -217
- package/dist/cache.d.ts +24 -14
- package/dist/cache.js +102 -66
- package/dist/catalog.d.ts +83 -0
- package/dist/catalog.js +293 -0
- package/dist/command.d.ts +1 -0
- package/dist/command.js +235 -68
- package/dist/fake.d.ts +4 -0
- package/dist/fake.js +142 -0
- package/dist/generate.d.ts +28 -24
- package/dist/generate.js +417 -120
- package/dist/inputs.d.ts +12 -0
- package/dist/inputs.js +132 -0
- package/dist/knowledge.d.ts +50 -0
- package/dist/knowledge.js +199 -0
- package/dist/logger.d.ts +10 -8
- package/dist/logger.js +4 -3
- package/dist/paging.d.ts +2 -2
- package/dist/paging.js +2 -2
- package/dist/probe.d.ts +5 -5
- package/dist/probe.js +15 -15
- package/dist/prompt.d.ts +31 -18
- package/dist/prompt.js +160 -146
- package/dist/quiet.d.ts +2 -0
- package/dist/quiet.js +7 -0
- package/dist/router.js +5 -1
- package/dist/runtime.d.ts +160 -0
- package/dist/runtime.js +229 -0
- package/dist/schema.js +1 -2
- package/dist/server.js +98 -57
- package/dist/setup.d.ts +47 -6
- package/dist/setup.js +157 -21
- package/dist/spec.d.ts +4 -3
- package/dist/spec.js +73 -22
- package/dist/store.d.ts +78 -0
- package/dist/store.js +253 -0
- package/dist/synth.d.ts +42 -0
- package/dist/synth.js +589 -0
- package/dist/worker.d.ts +41 -0
- package/dist/worker.js +99 -0
- package/package.json +5 -3
- package/dist/envelope.d.ts +0 -13
- package/dist/envelope.js +0 -9
- package/dist/image.d.ts +0 -25
- package/dist/image.js +0 -81
package/dist/setup.js
CHANGED
|
@@ -1,30 +1,38 @@
|
|
|
1
|
-
import { credentialError,
|
|
2
|
-
import { createModel } from '@zenera/neo';
|
|
3
|
-
import { mkdirSync } from 'node:fs';
|
|
1
|
+
import { credentialError, CURATED, ensureHome, envNames, home, invalidError, KeyStore, paths, PROVIDERS, readJson, writeJson, } from '@zenera/cli/lib';
|
|
2
|
+
import { createEmbedder, createModel } from '@zenera/neo';
|
|
3
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
4
4
|
import { join, resolve } from 'node:path';
|
|
5
|
+
import { Bindings } from "./bindings.js";
|
|
5
6
|
import { Box } from "./box.js";
|
|
6
7
|
import { Cache } from "./cache.js";
|
|
7
|
-
import {
|
|
8
|
+
import { Catalog } from "./catalog.js";
|
|
9
|
+
import { checkTexts, expandSchemas } from "./inputs.js";
|
|
10
|
+
import { openKnowledge } from "./knowledge.js";
|
|
8
11
|
import { Router } from "./router.js";
|
|
9
12
|
import { loadSpecs, SpecError } from "./spec.js";
|
|
13
|
+
import { listWorlds, worldPath, WorldStore } from "./store.js";
|
|
14
|
+
import { World } from "./synth.js";
|
|
10
15
|
import { Checks } from "./validate.js";
|
|
11
16
|
// ---------------------------------------------------------------------------
|
|
12
17
|
// Assembly
|
|
13
18
|
//
|
|
14
19
|
// The order here is the whole point and it is the same order `zen run` uses:
|
|
15
|
-
// credentials before anything that needs one, the
|
|
16
|
-
//
|
|
17
|
-
// user sees is the first thing that was actually wrong rather than
|
|
18
|
-
// happened to be checked first.
|
|
20
|
+
// credentials before anything that needs one, then the documents, then the
|
|
21
|
+
// story, and the world last because its file is named after the story — so the
|
|
22
|
+
// failure a user sees is the first thing that was actually wrong rather than
|
|
23
|
+
// whatever happened to be checked first.
|
|
19
24
|
// ---------------------------------------------------------------------------
|
|
20
25
|
/**
|
|
21
26
|
* The model each provider gets when none is named — the same picks `zen meta`
|
|
22
27
|
* recommends (`RECOMMENDED` in `packages/cli/src/meta/model.ts`). Every ref names its
|
|
23
28
|
* provider: the shorthand reads the first segment as a *provider name*, so a
|
|
24
29
|
* bare `gemini-3.8-flash` would be asked of OpenAI.
|
|
30
|
+
*
|
|
31
|
+
* Gym's agents call tools, and OpenAI's reasoning models take tools only over
|
|
32
|
+
* the Responses API, so that is the one named.
|
|
25
33
|
*/
|
|
26
34
|
const DEFAULT_MODEL = {
|
|
27
|
-
openai: 'openai:gpt-5.6-sol',
|
|
35
|
+
openai: 'openai/responses:gpt-5.6-sol',
|
|
28
36
|
anthropic: 'anthropic:claude-opus-5',
|
|
29
37
|
google: 'google:gemini-3.8-flash',
|
|
30
38
|
vertex: 'vertex:gemini-3.8-flash',
|
|
@@ -35,7 +43,7 @@ const DEFAULT_MODEL = {
|
|
|
35
43
|
//
|
|
36
44
|
// The same chain `zen meta` uses: a flag beats the environment, the environment
|
|
37
45
|
// beats what was stored, and with none of them set it is whatever the keys on
|
|
38
|
-
// hand can buy. Which model writes
|
|
46
|
+
// hand can buy. Which model writes handlers is a choice about a tool on this
|
|
39
47
|
// machine and not about a document, so it is stored beside the keyring rather
|
|
40
48
|
// than anywhere near a specification.
|
|
41
49
|
// ---------------------------------------------------------------------------
|
|
@@ -70,9 +78,54 @@ export async function chooseModel(flag, keys) {
|
|
|
70
78
|
}
|
|
71
79
|
return undefined;
|
|
72
80
|
}
|
|
81
|
+
/** The variable that names the embedding model, or `none`. */
|
|
82
|
+
export const EMBEDDING_ENV = 'ZENERA_GYM_EMBEDDING';
|
|
83
|
+
/** `--embedding` values that ask for whichever model is configured. */
|
|
84
|
+
const EMBEDDING_ON = new Set(['on', 'default']);
|
|
85
|
+
/** `--embedding` values that keep it off, which is also what no flag means. */
|
|
86
|
+
const EMBEDDING_OFF = new Set(['', 'off', 'none']);
|
|
87
|
+
/**
|
|
88
|
+
* Which model embeds the docs, if any. Off unless the flag asks: a ref names
|
|
89
|
+
* the model outright; `on` (or `default`) takes the configured one — the
|
|
90
|
+
* variable, then the stored setting, then the first keyed provider that offers
|
|
91
|
+
* one. The variable and the setting only answer that question; on their own
|
|
92
|
+
* they turn nothing on.
|
|
93
|
+
*/
|
|
94
|
+
export async function chooseEmbedding(flag, keys) {
|
|
95
|
+
const asked = flag?.trim().toLowerCase() ?? '';
|
|
96
|
+
if (EMBEDDING_OFF.has(asked)) {
|
|
97
|
+
return { from: 'none' };
|
|
98
|
+
}
|
|
99
|
+
if (!EMBEDDING_ON.has(asked)) {
|
|
100
|
+
return { ref: flag.trim(), from: 'flag' };
|
|
101
|
+
}
|
|
102
|
+
const configured = await configuredEmbedding(keys);
|
|
103
|
+
if (configured.from === 'none') {
|
|
104
|
+
throw credentialError('--embedding on, but there is no embedding model to use', `name one with --embedding <ref>, set ${EMBEDDING_ENV}, ` +
|
|
105
|
+
'or add a key for a provider that offers one: zen key add openai');
|
|
106
|
+
}
|
|
107
|
+
return configured;
|
|
108
|
+
}
|
|
109
|
+
/** The model `--embedding on` would use; `none` at a link means off. */
|
|
110
|
+
export async function configuredEmbedding(keys) {
|
|
111
|
+
const chain = [
|
|
112
|
+
['env', process.env[EMBEDDING_ENV]],
|
|
113
|
+
['store', (await readSettings()).embedding],
|
|
114
|
+
['default', defaultEmbedding(keys)],
|
|
115
|
+
];
|
|
116
|
+
for (const [from, ref] of chain) {
|
|
117
|
+
const named = ref?.trim();
|
|
118
|
+
if (named) {
|
|
119
|
+
return EMBEDDING_OFF.has(named.toLowerCase()) ? { from } : { ref: named, from };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return { from: 'none' };
|
|
123
|
+
}
|
|
73
124
|
export async function open(opts) {
|
|
74
|
-
|
|
75
|
-
|
|
125
|
+
const story = opts.story ?? [];
|
|
126
|
+
const docsArgs = opts.docs ?? [];
|
|
127
|
+
if (opts.schemas.length === 0) {
|
|
128
|
+
throw invalidError('no API document given', 'name OpenAPI/Swagger files, directories or patterns, e.g. zen gym serve api/');
|
|
76
129
|
}
|
|
77
130
|
// Real environment variables win, exactly as they do for `zen`.
|
|
78
131
|
ensureHome();
|
|
@@ -85,29 +138,101 @@ export async function open(opts) {
|
|
|
85
138
|
const model = createModel(choice.ref);
|
|
86
139
|
const root = resolve(opts.cwd, opts.cache ?? paths.gym());
|
|
87
140
|
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
await
|
|
91
|
-
const
|
|
92
|
-
const operations = await loadSpecs(opts.specs.map((s) => resolve(opts.cwd, s)));
|
|
141
|
+
const files = await expandSchemas(opts.schemas, opts.cwd);
|
|
142
|
+
await checkTexts(story, opts.cwd, '--story');
|
|
143
|
+
await checkTexts(docsArgs, opts.cwd, '--docs');
|
|
144
|
+
const operations = await loadSpecs(files);
|
|
93
145
|
if (operations.length === 0) {
|
|
94
|
-
throw invalidError('the
|
|
146
|
+
throw invalidError('the API documents declare no operations');
|
|
95
147
|
}
|
|
96
148
|
const router = new Router(operations);
|
|
97
149
|
const checks = new Checks();
|
|
98
|
-
const
|
|
99
|
-
|
|
150
|
+
const catalog = await Catalog.open(files);
|
|
151
|
+
// Everything is indexed before anything is served: no request ever waits
|
|
152
|
+
// on, or works around, an index that is still being built.
|
|
153
|
+
const embedding = docsArgs.length > 0
|
|
154
|
+
? await chooseEmbedding(opts.embedding, keys)
|
|
155
|
+
: { from: 'none' };
|
|
156
|
+
const knowledge = await openKnowledge({
|
|
157
|
+
story,
|
|
158
|
+
docs: docsArgs,
|
|
159
|
+
cwd: opts.cwd,
|
|
160
|
+
root,
|
|
161
|
+
embedding: embedding.ref
|
|
162
|
+
? { ref: embedding.ref, embedder: createEmbedder(embedding.ref) }
|
|
163
|
+
: undefined,
|
|
164
|
+
onWarn: opts.onWarn,
|
|
165
|
+
onProgress: opts.onIndexing,
|
|
166
|
+
});
|
|
167
|
+
const worldFile = worldPath({
|
|
168
|
+
root,
|
|
169
|
+
cwd: opts.cwd,
|
|
170
|
+
world: opts.world,
|
|
171
|
+
storyDigest: knowledge.storyDigest || undefined,
|
|
172
|
+
});
|
|
173
|
+
if (!opts.world && knowledge.storyDigest && !existsSync(worldFile)) {
|
|
174
|
+
const earlier = listWorlds(root).find((w) => w.name.startsWith('story-'));
|
|
175
|
+
if (earlier) {
|
|
176
|
+
opts.onWarn?.(`a new story, so a new world: ${worldFile}. The last one used is ${earlier.path}; ` +
|
|
177
|
+
'name a world with --world to keep one across story edits');
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const store = new WorldStore(worldFile);
|
|
181
|
+
if (opts.reset) {
|
|
182
|
+
store.reset();
|
|
183
|
+
}
|
|
184
|
+
const bindings = new Bindings(Object.keys(catalog.types), { ephemeral: opts.ephemeral });
|
|
185
|
+
const world = new World({
|
|
186
|
+
model,
|
|
187
|
+
knowledge,
|
|
188
|
+
catalog,
|
|
189
|
+
bindings,
|
|
190
|
+
store,
|
|
191
|
+
reportDir: join(root, 'reports'),
|
|
192
|
+
onMade: opts.onWorld,
|
|
193
|
+
});
|
|
194
|
+
const box = new Box({
|
|
195
|
+
root,
|
|
196
|
+
db: store.path,
|
|
197
|
+
timeout: opts.timeout,
|
|
198
|
+
synthesize: world.synthesize,
|
|
199
|
+
bindings: () => bindings.all(),
|
|
200
|
+
});
|
|
100
201
|
const cache = new Cache({
|
|
101
202
|
box,
|
|
102
203
|
checks,
|
|
103
204
|
model,
|
|
205
|
+
catalog,
|
|
206
|
+
world,
|
|
207
|
+
bindings,
|
|
208
|
+
operations,
|
|
104
209
|
attempts: opts.attempts,
|
|
105
210
|
concurrency: opts.concurrency,
|
|
106
211
|
rebuild: opts.rebuild,
|
|
107
212
|
ephemeral: opts.ephemeral,
|
|
108
213
|
...opts.events,
|
|
109
214
|
});
|
|
110
|
-
return {
|
|
215
|
+
return {
|
|
216
|
+
router,
|
|
217
|
+
checks,
|
|
218
|
+
cache,
|
|
219
|
+
box,
|
|
220
|
+
model,
|
|
221
|
+
choice,
|
|
222
|
+
catalog,
|
|
223
|
+
knowledge,
|
|
224
|
+
embedding,
|
|
225
|
+
files,
|
|
226
|
+
world,
|
|
227
|
+
worldFile,
|
|
228
|
+
root,
|
|
229
|
+
close: async () => {
|
|
230
|
+
await box.dispose();
|
|
231
|
+
world.close();
|
|
232
|
+
store.close();
|
|
233
|
+
knowledge.close();
|
|
234
|
+
},
|
|
235
|
+
};
|
|
111
236
|
}
|
|
112
237
|
/**
|
|
113
238
|
* Presence, not liveness. `zen init` probes because it is writing a project
|
|
@@ -119,5 +244,16 @@ function defaultRef(keys) {
|
|
|
119
244
|
PROVIDERS.find((p) => keys.active(p) !== undefined);
|
|
120
245
|
return provider ? DEFAULT_MODEL[provider] : undefined;
|
|
121
246
|
}
|
|
247
|
+
/** The first keyed provider, in preference order, with a curated embedding model. */
|
|
248
|
+
function defaultEmbedding(keys) {
|
|
249
|
+
const keyed = PROVIDERS.filter((p) => envNames(p).some((name) => process.env[name]) || keys.active(p) !== undefined);
|
|
250
|
+
for (const provider of keyed) {
|
|
251
|
+
const model = CURATED[provider].find((m) => m.roles.includes('embedding'));
|
|
252
|
+
if (model) {
|
|
253
|
+
return `${provider}:${model.id}`;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
122
258
|
export { SpecError };
|
|
123
259
|
//# sourceMappingURL=setup.js.map
|
package/dist/spec.d.ts
CHANGED
|
@@ -2,10 +2,11 @@ import { CliError } from '@zenera/cli/lib';
|
|
|
2
2
|
import { type Paging } from './paging.ts';
|
|
3
3
|
import { type Schema } from './schema.ts';
|
|
4
4
|
/**
|
|
5
|
-
* Version of
|
|
6
|
-
* Bumping
|
|
5
|
+
* Version of the handler contract: the prompt rules and the `ctx` runtime.
|
|
6
|
+
* Bumping it gives every operation a new cache key, so nothing written to an
|
|
7
|
+
* older contract is ever run against a newer one.
|
|
7
8
|
*/
|
|
8
|
-
export declare const
|
|
9
|
+
export declare const CONTRACT_VERSION = 4;
|
|
9
10
|
export declare const METHODS: readonly ["get", "put", "post", "delete", "patch", "head", "options"];
|
|
10
11
|
export type Method = (typeof METHODS)[number];
|
|
11
12
|
export type ParamIn = 'path' | 'query' | 'header' | 'cookie';
|
package/dist/spec.js
CHANGED
|
@@ -4,10 +4,11 @@ import { createHash } from 'node:crypto';
|
|
|
4
4
|
import { pagingOf } from "./paging.js";
|
|
5
5
|
import { normalize } from "./schema.js";
|
|
6
6
|
/**
|
|
7
|
-
* Version of
|
|
8
|
-
* Bumping
|
|
7
|
+
* Version of the handler contract: the prompt rules and the `ctx` runtime.
|
|
8
|
+
* Bumping it gives every operation a new cache key, so nothing written to an
|
|
9
|
+
* older contract is ever run against a newer one.
|
|
9
10
|
*/
|
|
10
|
-
export const
|
|
11
|
+
export const CONTRACT_VERSION = 4;
|
|
11
12
|
// ---------------------------------------------------------------------------
|
|
12
13
|
// Documents, flattened
|
|
13
14
|
//
|
|
@@ -16,9 +17,10 @@ export const GENERATOR_RULES_VERSION = 2;
|
|
|
16
17
|
// document it came from, and which of the three dialects that document was
|
|
17
18
|
// written in, stops mattering here.
|
|
18
19
|
//
|
|
19
|
-
// `dereference` is used rather than `bundle` on purpose:
|
|
20
|
-
//
|
|
21
|
-
//
|
|
20
|
+
// `dereference` is used rather than `bundle` on purpose: requests and responses
|
|
21
|
+
// are checked one operation at a time, and a check wants the whole shape in
|
|
22
|
+
// hand. The agents see the other view — named, bundled types — through
|
|
23
|
+
// catalog.ts.
|
|
22
24
|
// ---------------------------------------------------------------------------
|
|
23
25
|
export const METHODS = ['get', 'put', 'post', 'delete', 'patch', 'head', 'options'];
|
|
24
26
|
const isObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
@@ -116,7 +118,7 @@ function splitQuery(template) {
|
|
|
116
118
|
const path = template.slice(0, cut) || '/';
|
|
117
119
|
return Object.keys(fixed).length > 0 ? { path, fixed } : { path };
|
|
118
120
|
}
|
|
119
|
-
/** A one-value enum, so the check, the page and the
|
|
121
|
+
/** A one-value enum, so the check, the page and the handler all see the rule. */
|
|
120
122
|
function pinned(fixed) {
|
|
121
123
|
return Object.entries(fixed ?? {}).map(([name, value]) => ({
|
|
122
124
|
name,
|
|
@@ -147,32 +149,81 @@ function build(b) {
|
|
|
147
149
|
return { ...operation, key: keyOf(operation) };
|
|
148
150
|
}
|
|
149
151
|
/**
|
|
150
|
-
* Cache identity. Everything that changes what a
|
|
152
|
+
* Cache identity. Everything that changes what a handler must produce is in
|
|
151
153
|
* here and nothing else is — not the file it came from, not its summary, not
|
|
152
|
-
* the
|
|
153
|
-
*
|
|
154
|
-
* to get wrong.
|
|
154
|
+
* the wording of a field's description, not the order of its keys. Editing a
|
|
155
|
+
* spec's shape therefore yields a new key and the old artefact is simply never
|
|
156
|
+
* looked up again, so there is no invalidation step to get wrong.
|
|
157
|
+
*
|
|
158
|
+
* The world's type bindings are part of a handler's identity too, but they are
|
|
159
|
+
* known only once it is written; `cache.ts` adds them on top of this key.
|
|
155
160
|
*/
|
|
156
161
|
function keyOf(op) {
|
|
157
162
|
const shape = {
|
|
158
|
-
version:
|
|
163
|
+
version: CONTRACT_VERSION,
|
|
159
164
|
method: op.method,
|
|
160
165
|
path: op.path,
|
|
161
166
|
operationId: op.operationId,
|
|
162
167
|
params: [...op.params]
|
|
163
168
|
.sort((a, b) => `${a.in}:${a.name}`.localeCompare(`${b.in}:${b.name}`))
|
|
164
|
-
.map((p) => [p.in, p.name, p.required, canonical(p.schema)]),
|
|
165
|
-
body: op.requestBody
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
// paging, so that everything else hashes to exactly what it did before
|
|
171
|
-
// and no one else is asked to rebuild.
|
|
172
|
-
...(op.paging ? { paging: canonical(op.paging) } : {}),
|
|
169
|
+
.map((p) => [p.in, p.name, p.required, canonical(bare(p.schema))]),
|
|
170
|
+
body: op.requestBody
|
|
171
|
+
? [op.requestBody.required, canonical(bare(op.requestBody.schema))]
|
|
172
|
+
: null,
|
|
173
|
+
success: [op.success.status, op.success.schema ? canonical(bare(op.success.schema)) : null],
|
|
174
|
+
paging: op.paging ? canonical(op.paging) : null,
|
|
173
175
|
};
|
|
174
176
|
return createHash('sha256').update(JSON.stringify(shape)).digest('hex').slice(0, 16);
|
|
175
177
|
}
|
|
178
|
+
/** Keywords that annotate a schema and change nothing a handler must produce. */
|
|
179
|
+
const ANNOTATIONS = new Set([
|
|
180
|
+
'$schema',
|
|
181
|
+
'$comment',
|
|
182
|
+
'title',
|
|
183
|
+
'description',
|
|
184
|
+
'example',
|
|
185
|
+
'examples',
|
|
186
|
+
'externalDocs',
|
|
187
|
+
'deprecated',
|
|
188
|
+
]);
|
|
189
|
+
/** Keywords whose value is a map of names to schemas, not a schema itself. */
|
|
190
|
+
const SCHEMA_MAPS = new Set([
|
|
191
|
+
'properties',
|
|
192
|
+
'patternProperties',
|
|
193
|
+
'$defs',
|
|
194
|
+
'definitions',
|
|
195
|
+
'dependentSchemas',
|
|
196
|
+
]);
|
|
197
|
+
/** Keywords whose value is data, kept verbatim whatever keys it happens to have. */
|
|
198
|
+
const DATA = new Set(['enum', 'const', 'default']);
|
|
199
|
+
/**
|
|
200
|
+
* A schema without its annotations. Schema-aware rather than a blanket key
|
|
201
|
+
* filter, because a *property* called `description` is shape, not prose.
|
|
202
|
+
*/
|
|
203
|
+
function bare(schema) {
|
|
204
|
+
if (Array.isArray(schema)) {
|
|
205
|
+
return schema.map(bare);
|
|
206
|
+
}
|
|
207
|
+
if (!isObject(schema)) {
|
|
208
|
+
return schema;
|
|
209
|
+
}
|
|
210
|
+
const out = {};
|
|
211
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
212
|
+
if (ANNOTATIONS.has(key) || key.startsWith('x-')) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (DATA.has(key)) {
|
|
216
|
+
out[key] = value;
|
|
217
|
+
}
|
|
218
|
+
else if (SCHEMA_MAPS.has(key) && isObject(value)) {
|
|
219
|
+
out[key] = Object.fromEntries(Object.entries(value).map(([k, v]) => [k, bare(v)]));
|
|
220
|
+
}
|
|
221
|
+
else {
|
|
222
|
+
out[key] = bare(value);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return out;
|
|
226
|
+
}
|
|
176
227
|
/** Key order is a formatting accident; it must not change a cache key. */
|
|
177
228
|
function canonical(value) {
|
|
178
229
|
if (Array.isArray(value)) {
|
|
@@ -262,7 +313,7 @@ function requestBody(op, dialect) {
|
|
|
262
313
|
* The response a call gets answered with: the lowest 2xx that carries a JSON
|
|
263
314
|
* body, or the lowest 2xx at all when none of them do. An operation whose
|
|
264
315
|
* success response has no JSON schema is answered `204` and never gets a
|
|
265
|
-
*
|
|
316
|
+
* handler — there is nothing for one to produce.
|
|
266
317
|
*/
|
|
267
318
|
function successOf(op, dialect) {
|
|
268
319
|
const responses = isObject(op.responses) ? op.responses : {};
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
export interface CollectionSummary {
|
|
3
|
+
name: string;
|
|
4
|
+
count: number;
|
|
5
|
+
sampleIds: string[];
|
|
6
|
+
}
|
|
7
|
+
export interface Collection<T = Record<string, unknown>> {
|
|
8
|
+
readonly name: string;
|
|
9
|
+
count(): number;
|
|
10
|
+
all(): T[];
|
|
11
|
+
get(id: string | number): T | undefined;
|
|
12
|
+
has(id: string | number): boolean;
|
|
13
|
+
find(predicate: (item: T) => boolean): T[];
|
|
14
|
+
put(id: string | number, value: T): T;
|
|
15
|
+
putMany(items: readonly {
|
|
16
|
+
id: string | number;
|
|
17
|
+
value: T;
|
|
18
|
+
}[]): number;
|
|
19
|
+
delete(id: string | number): boolean;
|
|
20
|
+
clear(): void;
|
|
21
|
+
}
|
|
22
|
+
export declare class WorldStore {
|
|
23
|
+
#private;
|
|
24
|
+
readonly path: string;
|
|
25
|
+
constructor(path: string);
|
|
26
|
+
collection<T = Record<string, unknown>>(name: string): Collection<T>;
|
|
27
|
+
collections(): CollectionSummary[];
|
|
28
|
+
getMeta<T = unknown>(key: string): T | undefined;
|
|
29
|
+
setMeta(key: string, value: unknown): void;
|
|
30
|
+
/**
|
|
31
|
+
* A consistent copy of the whole world, for judging a handler without
|
|
32
|
+
* letting its test traffic leak into what real requests will see.
|
|
33
|
+
*/
|
|
34
|
+
snapshot(to: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* Takes a lease, or says someone else holds it. A lease is a row in `meta`
|
|
37
|
+
* claimed inside one immediate transaction, so two processes on the same
|
|
38
|
+
* file cannot both win; it lapses when its time is up or its process is
|
|
39
|
+
* gone, so a crash costs nobody a wait.
|
|
40
|
+
*/
|
|
41
|
+
claim(key: string, owner: string, ttlMs: number): boolean;
|
|
42
|
+
/** Gives a lease back, if it is still this owner's. */
|
|
43
|
+
release(key: string, owner: string): void;
|
|
44
|
+
/** Throws away every entity, keeping the file and its schema. */
|
|
45
|
+
reset(): void;
|
|
46
|
+
close(): void;
|
|
47
|
+
/** @internal */
|
|
48
|
+
get raw(): DatabaseSync;
|
|
49
|
+
}
|
|
50
|
+
/** A lease owner for this process. */
|
|
51
|
+
export declare const leaseOwner: () => string;
|
|
52
|
+
/**
|
|
53
|
+
* Where the world lives. `--world` wins: a name is a file under
|
|
54
|
+
* `<root>/worlds`, anything that looks like a path is that path. Otherwise the
|
|
55
|
+
* story names its own world, keyed by what the story says, so two processes
|
|
56
|
+
* handed the same story share one world without being told to; with neither,
|
|
57
|
+
* there is one default world.
|
|
58
|
+
*/
|
|
59
|
+
export declare function worldPath(opts: {
|
|
60
|
+
root: string;
|
|
61
|
+
cwd: string;
|
|
62
|
+
world?: string;
|
|
63
|
+
storyDigest?: string;
|
|
64
|
+
}): string;
|
|
65
|
+
export declare const worldsDir: (root: string) => string;
|
|
66
|
+
export interface WorldFile {
|
|
67
|
+
name: string;
|
|
68
|
+
path: string;
|
|
69
|
+
bytes: number;
|
|
70
|
+
usedAt: Date;
|
|
71
|
+
collections: CollectionSummary[];
|
|
72
|
+
}
|
|
73
|
+
/** Every world under `<root>/worlds`, most recently used first. */
|
|
74
|
+
export declare function listWorlds(root: string): WorldFile[];
|
|
75
|
+
/** Deletes a world and the journal files SQLite keeps beside it. */
|
|
76
|
+
export declare function removeWorld(path: string): void;
|
|
77
|
+
export declare const digest: (text: string) => string;
|
|
78
|
+
//# sourceMappingURL=store.d.ts.map
|