@condition-sh/core 2026.9.2 → 2026.9.3
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/index.js +91 -8
- package/local.js +95 -12
- package/package.json +1 -1
- package/types/index.d.ts +16 -8
- package/types/store.d.ts +1 -0
- package/types/traits.d.ts +25 -0
package/index.js
CHANGED
|
@@ -98,6 +98,74 @@ function validateInput(schema, value) {
|
|
|
98
98
|
check(schema, value, "input", errors);
|
|
99
99
|
return errors;
|
|
100
100
|
}
|
|
101
|
+
|
|
102
|
+
// packages/core/src/traits.ts
|
|
103
|
+
var isList = (values) => Array.isArray(values);
|
|
104
|
+
function describeValues(values) {
|
|
105
|
+
return isList(values) ? values.join(", ") : `an integer from ${values.minimum} to ${values.maximum}`;
|
|
106
|
+
}
|
|
107
|
+
function accepts(values, value) {
|
|
108
|
+
if (isList(values))
|
|
109
|
+
return values.includes(value);
|
|
110
|
+
return Number.isInteger(value) && value >= values.minimum && value <= values.maximum;
|
|
111
|
+
}
|
|
112
|
+
function requirementProblems(specs, resolved) {
|
|
113
|
+
const problems = [];
|
|
114
|
+
for (const [name, value] of Object.entries(resolved)) {
|
|
115
|
+
for (const [other, allowed] of Object.entries(specs[name].requires?.[String(value)] ?? {})) {
|
|
116
|
+
if (allowed.includes(resolved[other]))
|
|
117
|
+
continue;
|
|
118
|
+
const actual = other in resolved ? `, not "${resolved[other]}"` : "";
|
|
119
|
+
problems.push(`${name} "${value}" needs ${other} to be one of ${allowed.join(", ")}${actual}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return problems;
|
|
123
|
+
}
|
|
124
|
+
function resolveTraits(specs, requested = {}) {
|
|
125
|
+
if (requested === null || typeof requested !== "object" || Array.isArray(requested))
|
|
126
|
+
throw new Error("traits must be an object");
|
|
127
|
+
const asked = requested;
|
|
128
|
+
const available = specs ?? {};
|
|
129
|
+
const problems = [];
|
|
130
|
+
const resolved = {};
|
|
131
|
+
for (const name of Object.keys(asked)) {
|
|
132
|
+
if (!(name in available))
|
|
133
|
+
problems.push(`unknown trait "${name}"; choose from ${Object.keys(available).join(", ") || "none"}`);
|
|
134
|
+
}
|
|
135
|
+
for (const [name, spec] of Object.entries(available)) {
|
|
136
|
+
const value = name in asked ? asked[name] : spec.default;
|
|
137
|
+
if (value === undefined)
|
|
138
|
+
continue;
|
|
139
|
+
if (accepts(spec.values, value))
|
|
140
|
+
resolved[name] = value;
|
|
141
|
+
else
|
|
142
|
+
problems.push(`${name} can't be ${JSON.stringify(value)}; choose ${describeValues(spec.values)}`);
|
|
143
|
+
}
|
|
144
|
+
if (!problems.length)
|
|
145
|
+
problems.push(...requirementProblems(available, resolved));
|
|
146
|
+
if (problems.length)
|
|
147
|
+
throw new Error(`invalid traits: ${problems.join("; ")}`);
|
|
148
|
+
return resolved;
|
|
149
|
+
}
|
|
150
|
+
function summarizeTraits(specs) {
|
|
151
|
+
return Object.fromEntries(Object.entries(specs).map(([name, { description, values, default: fallback, requires }]) => [name, {
|
|
152
|
+
...description ? { description } : {},
|
|
153
|
+
values,
|
|
154
|
+
...fallback === undefined ? {} : { default: fallback },
|
|
155
|
+
...requires ? { requires } : {}
|
|
156
|
+
}]));
|
|
157
|
+
}
|
|
158
|
+
async function buildState(prepare, specs, context) {
|
|
159
|
+
let output = await prepare(context);
|
|
160
|
+
for (const [name, spec] of Object.entries(specs ?? {})) {
|
|
161
|
+
if (!(name in context.traits))
|
|
162
|
+
continue;
|
|
163
|
+
const next = await spec.apply({ ...context, output, value: context.traits[name] });
|
|
164
|
+
if (next !== undefined)
|
|
165
|
+
output = next;
|
|
166
|
+
}
|
|
167
|
+
return output;
|
|
168
|
+
}
|
|
101
169
|
// packages/core/src/remote-inbox.ts
|
|
102
170
|
import { createHash } from "node:crypto";
|
|
103
171
|
var actorPattern = /^[a-z][a-z0-9-]{0,15}$/;
|
|
@@ -203,7 +271,8 @@ function describeProject(project) {
|
|
|
203
271
|
name,
|
|
204
272
|
description: scenario.description,
|
|
205
273
|
...scenario.input ? { input: scenario.input } : {},
|
|
206
|
-
...scenario.actors ? { actors: scenario.actors } : {}
|
|
274
|
+
...scenario.actors ? { actors: scenario.actors } : {},
|
|
275
|
+
...scenario.traits ? { traits: summarizeTraits(scenario.traits) } : {}
|
|
207
276
|
}))
|
|
208
277
|
};
|
|
209
278
|
}
|
|
@@ -213,6 +282,7 @@ function publicRun(run, project) {
|
|
|
213
282
|
project,
|
|
214
283
|
scenario: run.scenario,
|
|
215
284
|
input: parse(run.presentedInput),
|
|
285
|
+
traits: JSON.parse(run.traits),
|
|
216
286
|
output: parse(run.presentedOutput),
|
|
217
287
|
status: run.status,
|
|
218
288
|
leaseUntil: run.leaseUntil,
|
|
@@ -273,9 +343,17 @@ class Condition {
|
|
|
273
343
|
}
|
|
274
344
|
async start(name, input = {}, options = {}) {
|
|
275
345
|
const scenario = this.scenario(name);
|
|
276
|
-
const
|
|
346
|
+
const inputProblems = scenario.input ? validateInput(scenario.input, input) : [];
|
|
347
|
+
const problems = inputProblems.length ? [`invalid input: ${inputProblems.join("; ")}`] : [];
|
|
348
|
+
let traits = {};
|
|
349
|
+
try {
|
|
350
|
+
traits = resolveTraits(scenario.traits, options.traits);
|
|
351
|
+
} catch (error) {
|
|
352
|
+
problems.push(message(error));
|
|
353
|
+
}
|
|
277
354
|
if (problems.length)
|
|
278
|
-
throw new Error(
|
|
355
|
+
throw new Error(problems.join("; "));
|
|
356
|
+
const traitsJson = JSON.stringify(traits);
|
|
279
357
|
const wantsInbox = !scenario.actors || scenario.actors.some((actor) => actor.inbox);
|
|
280
358
|
const minutes = options.leaseMinutes ?? 20;
|
|
281
359
|
if (!Number.isInteger(minutes) || minutes < 1 || minutes > 1440)
|
|
@@ -290,6 +368,7 @@ class Condition {
|
|
|
290
368
|
id: crypto.randomUUID(),
|
|
291
369
|
scenario: name,
|
|
292
370
|
input: inputJson,
|
|
371
|
+
traits: traitsJson,
|
|
293
372
|
output: null,
|
|
294
373
|
status: "preparing",
|
|
295
374
|
leaseUntil: new Date(now.getTime() + minutes * 60000).toISOString(),
|
|
@@ -306,7 +385,7 @@ class Condition {
|
|
|
306
385
|
const existing = run.requestKey ? await this.store.findByRequestKey(run.requestKey) : null;
|
|
307
386
|
if (!existing)
|
|
308
387
|
throw new Error("run conflict");
|
|
309
|
-
if (existing.scenario !== name || existing.input !== inputJson)
|
|
388
|
+
if (existing.scenario !== name || existing.input !== inputJson || existing.traits !== traitsJson)
|
|
310
389
|
throw new Error("requestKey was used with different input");
|
|
311
390
|
return { run: this.publicRun(existing), created: false };
|
|
312
391
|
}
|
|
@@ -316,6 +395,7 @@ class Condition {
|
|
|
316
395
|
throw new Error(`run ${id} is ${run.status}`);
|
|
317
396
|
const scenario = this.scenario(run.scenario);
|
|
318
397
|
const input = JSON.parse(run.input);
|
|
398
|
+
const traits = JSON.parse(run.traits);
|
|
319
399
|
const inbox = this.project.inbox;
|
|
320
400
|
let step = "prepare";
|
|
321
401
|
try {
|
|
@@ -329,7 +409,7 @@ class Condition {
|
|
|
329
409
|
await inbox.reserve({ runId: id, project: this.project.name, leaseUntil: run.leaseUntil });
|
|
330
410
|
step = "prepare";
|
|
331
411
|
}
|
|
332
|
-
const output = await scenario.prepare
|
|
412
|
+
const output = await buildState(scenario.prepare, scenario.traits, { runId: id, input, traits, inbox: this.inboxFor(run) });
|
|
333
413
|
const saved = await this.store.update(id, "preparing", {
|
|
334
414
|
output: JSON.stringify(output ?? null),
|
|
335
415
|
presentedOutput: await presentation(scenario.present, output ?? null)
|
|
@@ -337,7 +417,7 @@ class Condition {
|
|
|
337
417
|
if (!saved)
|
|
338
418
|
throw new Error(`run ${id} changed during preparation`);
|
|
339
419
|
step = "verify";
|
|
340
|
-
await scenario.verify({ runId: id, input, output, inbox: this.inboxFor(run) });
|
|
420
|
+
await scenario.verify({ runId: id, input, traits, output, inbox: this.inboxFor(run) });
|
|
341
421
|
if (!await this.store.update(id, "preparing", { status: "ready" }))
|
|
342
422
|
throw new Error(`run ${id} changed during verification`);
|
|
343
423
|
} catch (error) {
|
|
@@ -360,7 +440,7 @@ class Condition {
|
|
|
360
440
|
if (!scenario.login)
|
|
361
441
|
throw new Error(`scenario ${run.scenario} does not provide login`);
|
|
362
442
|
this.checkActor(run, actor, "login");
|
|
363
|
-
return scenario.login({ runId: id, input: JSON.parse(run.input), output: parse(run.output), actor, inbox: this.inboxFor(run) });
|
|
443
|
+
return scenario.login({ runId: id, input: JSON.parse(run.input), traits: JSON.parse(run.traits), output: parse(run.output), actor, inbox: this.inboxFor(run) });
|
|
364
444
|
}
|
|
365
445
|
async inbox(id, actor) {
|
|
366
446
|
const run = await this.read(id);
|
|
@@ -394,7 +474,7 @@ class Condition {
|
|
|
394
474
|
throw new Error(`run ${id} is ${run.status}`);
|
|
395
475
|
const scenario = this.scenario(run.scenario);
|
|
396
476
|
try {
|
|
397
|
-
await scenario.cleanup({ runId: id, input: JSON.parse(run.input), output: parse(run.output), inbox: this.inboxFor(run) });
|
|
477
|
+
await scenario.cleanup({ runId: id, input: JSON.parse(run.input), traits: JSON.parse(run.traits), output: parse(run.output), inbox: this.inboxFor(run) });
|
|
398
478
|
if (run.inboxRef) {
|
|
399
479
|
if (!this.project.inbox || this.project.inbox.identity !== run.inboxRef) {
|
|
400
480
|
throw new Error(`inbox configuration for run ${id} is unavailable or changed`);
|
|
@@ -432,6 +512,8 @@ class Condition {
|
|
|
432
512
|
}
|
|
433
513
|
export {
|
|
434
514
|
validateInput,
|
|
515
|
+
summarizeTraits,
|
|
516
|
+
resolveTraits,
|
|
435
517
|
publicRun,
|
|
436
518
|
loadProject,
|
|
437
519
|
legacyInboxDomain,
|
|
@@ -439,6 +521,7 @@ export {
|
|
|
439
521
|
inboxAddress,
|
|
440
522
|
describeProject,
|
|
441
523
|
defineProject,
|
|
524
|
+
buildState,
|
|
442
525
|
RemoteInbox,
|
|
443
526
|
Condition
|
|
444
527
|
};
|
package/local.js
CHANGED
|
@@ -98,6 +98,74 @@ function validateInput(schema, value) {
|
|
|
98
98
|
check(schema, value, "input", errors);
|
|
99
99
|
return errors;
|
|
100
100
|
}
|
|
101
|
+
|
|
102
|
+
// packages/core/src/traits.ts
|
|
103
|
+
var isList = (values) => Array.isArray(values);
|
|
104
|
+
function describeValues(values) {
|
|
105
|
+
return isList(values) ? values.join(", ") : `an integer from ${values.minimum} to ${values.maximum}`;
|
|
106
|
+
}
|
|
107
|
+
function accepts(values, value) {
|
|
108
|
+
if (isList(values))
|
|
109
|
+
return values.includes(value);
|
|
110
|
+
return Number.isInteger(value) && value >= values.minimum && value <= values.maximum;
|
|
111
|
+
}
|
|
112
|
+
function requirementProblems(specs, resolved) {
|
|
113
|
+
const problems = [];
|
|
114
|
+
for (const [name, value] of Object.entries(resolved)) {
|
|
115
|
+
for (const [other, allowed] of Object.entries(specs[name].requires?.[String(value)] ?? {})) {
|
|
116
|
+
if (allowed.includes(resolved[other]))
|
|
117
|
+
continue;
|
|
118
|
+
const actual = other in resolved ? `, not "${resolved[other]}"` : "";
|
|
119
|
+
problems.push(`${name} "${value}" needs ${other} to be one of ${allowed.join(", ")}${actual}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return problems;
|
|
123
|
+
}
|
|
124
|
+
function resolveTraits(specs, requested = {}) {
|
|
125
|
+
if (requested === null || typeof requested !== "object" || Array.isArray(requested))
|
|
126
|
+
throw new Error("traits must be an object");
|
|
127
|
+
const asked = requested;
|
|
128
|
+
const available = specs ?? {};
|
|
129
|
+
const problems = [];
|
|
130
|
+
const resolved = {};
|
|
131
|
+
for (const name of Object.keys(asked)) {
|
|
132
|
+
if (!(name in available))
|
|
133
|
+
problems.push(`unknown trait "${name}"; choose from ${Object.keys(available).join(", ") || "none"}`);
|
|
134
|
+
}
|
|
135
|
+
for (const [name, spec] of Object.entries(available)) {
|
|
136
|
+
const value = name in asked ? asked[name] : spec.default;
|
|
137
|
+
if (value === undefined)
|
|
138
|
+
continue;
|
|
139
|
+
if (accepts(spec.values, value))
|
|
140
|
+
resolved[name] = value;
|
|
141
|
+
else
|
|
142
|
+
problems.push(`${name} can't be ${JSON.stringify(value)}; choose ${describeValues(spec.values)}`);
|
|
143
|
+
}
|
|
144
|
+
if (!problems.length)
|
|
145
|
+
problems.push(...requirementProblems(available, resolved));
|
|
146
|
+
if (problems.length)
|
|
147
|
+
throw new Error(`invalid traits: ${problems.join("; ")}`);
|
|
148
|
+
return resolved;
|
|
149
|
+
}
|
|
150
|
+
function summarizeTraits(specs) {
|
|
151
|
+
return Object.fromEntries(Object.entries(specs).map(([name, { description, values, default: fallback, requires }]) => [name, {
|
|
152
|
+
...description ? { description } : {},
|
|
153
|
+
values,
|
|
154
|
+
...fallback === undefined ? {} : { default: fallback },
|
|
155
|
+
...requires ? { requires } : {}
|
|
156
|
+
}]));
|
|
157
|
+
}
|
|
158
|
+
async function buildState(prepare, specs, context) {
|
|
159
|
+
let output = await prepare(context);
|
|
160
|
+
for (const [name, spec] of Object.entries(specs ?? {})) {
|
|
161
|
+
if (!(name in context.traits))
|
|
162
|
+
continue;
|
|
163
|
+
const next = await spec.apply({ ...context, output, value: context.traits[name] });
|
|
164
|
+
if (next !== undefined)
|
|
165
|
+
output = next;
|
|
166
|
+
}
|
|
167
|
+
return output;
|
|
168
|
+
}
|
|
101
169
|
// packages/core/src/remote-inbox.ts
|
|
102
170
|
import { createHash } from "node:crypto";
|
|
103
171
|
var actorPattern = /^[a-z][a-z0-9-]{0,15}$/;
|
|
@@ -203,7 +271,8 @@ function describeProject(project) {
|
|
|
203
271
|
name,
|
|
204
272
|
description: scenario.description,
|
|
205
273
|
...scenario.input ? { input: scenario.input } : {},
|
|
206
|
-
...scenario.actors ? { actors: scenario.actors } : {}
|
|
274
|
+
...scenario.actors ? { actors: scenario.actors } : {},
|
|
275
|
+
...scenario.traits ? { traits: summarizeTraits(scenario.traits) } : {}
|
|
207
276
|
}))
|
|
208
277
|
};
|
|
209
278
|
}
|
|
@@ -213,6 +282,7 @@ function publicRun(run, project) {
|
|
|
213
282
|
project,
|
|
214
283
|
scenario: run.scenario,
|
|
215
284
|
input: parse(run.presentedInput),
|
|
285
|
+
traits: JSON.parse(run.traits),
|
|
216
286
|
output: parse(run.presentedOutput),
|
|
217
287
|
status: run.status,
|
|
218
288
|
leaseUntil: run.leaseUntil,
|
|
@@ -273,9 +343,17 @@ class Condition {
|
|
|
273
343
|
}
|
|
274
344
|
async start(name, input = {}, options = {}) {
|
|
275
345
|
const scenario = this.scenario(name);
|
|
276
|
-
const
|
|
346
|
+
const inputProblems = scenario.input ? validateInput(scenario.input, input) : [];
|
|
347
|
+
const problems = inputProblems.length ? [`invalid input: ${inputProblems.join("; ")}`] : [];
|
|
348
|
+
let traits = {};
|
|
349
|
+
try {
|
|
350
|
+
traits = resolveTraits(scenario.traits, options.traits);
|
|
351
|
+
} catch (error) {
|
|
352
|
+
problems.push(message(error));
|
|
353
|
+
}
|
|
277
354
|
if (problems.length)
|
|
278
|
-
throw new Error(
|
|
355
|
+
throw new Error(problems.join("; "));
|
|
356
|
+
const traitsJson = JSON.stringify(traits);
|
|
279
357
|
const wantsInbox = !scenario.actors || scenario.actors.some((actor) => actor.inbox);
|
|
280
358
|
const minutes = options.leaseMinutes ?? 20;
|
|
281
359
|
if (!Number.isInteger(minutes) || minutes < 1 || minutes > 1440)
|
|
@@ -290,6 +368,7 @@ class Condition {
|
|
|
290
368
|
id: crypto.randomUUID(),
|
|
291
369
|
scenario: name,
|
|
292
370
|
input: inputJson,
|
|
371
|
+
traits: traitsJson,
|
|
293
372
|
output: null,
|
|
294
373
|
status: "preparing",
|
|
295
374
|
leaseUntil: new Date(now.getTime() + minutes * 60000).toISOString(),
|
|
@@ -306,7 +385,7 @@ class Condition {
|
|
|
306
385
|
const existing = run.requestKey ? await this.store.findByRequestKey(run.requestKey) : null;
|
|
307
386
|
if (!existing)
|
|
308
387
|
throw new Error("run conflict");
|
|
309
|
-
if (existing.scenario !== name || existing.input !== inputJson)
|
|
388
|
+
if (existing.scenario !== name || existing.input !== inputJson || existing.traits !== traitsJson)
|
|
310
389
|
throw new Error("requestKey was used with different input");
|
|
311
390
|
return { run: this.publicRun(existing), created: false };
|
|
312
391
|
}
|
|
@@ -316,6 +395,7 @@ class Condition {
|
|
|
316
395
|
throw new Error(`run ${id} is ${run.status}`);
|
|
317
396
|
const scenario = this.scenario(run.scenario);
|
|
318
397
|
const input = JSON.parse(run.input);
|
|
398
|
+
const traits = JSON.parse(run.traits);
|
|
319
399
|
const inbox = this.project.inbox;
|
|
320
400
|
let step = "prepare";
|
|
321
401
|
try {
|
|
@@ -329,7 +409,7 @@ class Condition {
|
|
|
329
409
|
await inbox.reserve({ runId: id, project: this.project.name, leaseUntil: run.leaseUntil });
|
|
330
410
|
step = "prepare";
|
|
331
411
|
}
|
|
332
|
-
const output = await scenario.prepare
|
|
412
|
+
const output = await buildState(scenario.prepare, scenario.traits, { runId: id, input, traits, inbox: this.inboxFor(run) });
|
|
333
413
|
const saved = await this.store.update(id, "preparing", {
|
|
334
414
|
output: JSON.stringify(output ?? null),
|
|
335
415
|
presentedOutput: await presentation(scenario.present, output ?? null)
|
|
@@ -337,7 +417,7 @@ class Condition {
|
|
|
337
417
|
if (!saved)
|
|
338
418
|
throw new Error(`run ${id} changed during preparation`);
|
|
339
419
|
step = "verify";
|
|
340
|
-
await scenario.verify({ runId: id, input, output, inbox: this.inboxFor(run) });
|
|
420
|
+
await scenario.verify({ runId: id, input, traits, output, inbox: this.inboxFor(run) });
|
|
341
421
|
if (!await this.store.update(id, "preparing", { status: "ready" }))
|
|
342
422
|
throw new Error(`run ${id} changed during verification`);
|
|
343
423
|
} catch (error) {
|
|
@@ -360,7 +440,7 @@ class Condition {
|
|
|
360
440
|
if (!scenario.login)
|
|
361
441
|
throw new Error(`scenario ${run.scenario} does not provide login`);
|
|
362
442
|
this.checkActor(run, actor, "login");
|
|
363
|
-
return scenario.login({ runId: id, input: JSON.parse(run.input), output: parse(run.output), actor, inbox: this.inboxFor(run) });
|
|
443
|
+
return scenario.login({ runId: id, input: JSON.parse(run.input), traits: JSON.parse(run.traits), output: parse(run.output), actor, inbox: this.inboxFor(run) });
|
|
364
444
|
}
|
|
365
445
|
async inbox(id, actor) {
|
|
366
446
|
const run = await this.read(id);
|
|
@@ -394,7 +474,7 @@ class Condition {
|
|
|
394
474
|
throw new Error(`run ${id} is ${run.status}`);
|
|
395
475
|
const scenario = this.scenario(run.scenario);
|
|
396
476
|
try {
|
|
397
|
-
await scenario.cleanup({ runId: id, input: JSON.parse(run.input), output: parse(run.output), inbox: this.inboxFor(run) });
|
|
477
|
+
await scenario.cleanup({ runId: id, input: JSON.parse(run.input), traits: JSON.parse(run.traits), output: parse(run.output), inbox: this.inboxFor(run) });
|
|
398
478
|
if (run.inboxRef) {
|
|
399
479
|
if (!this.project.inbox || this.project.inbox.identity !== run.inboxRef) {
|
|
400
480
|
throw new Error(`inbox configuration for run ${id} is unavailable or changed`);
|
|
@@ -448,6 +528,7 @@ function fromRow(row) {
|
|
|
448
528
|
id: row.id,
|
|
449
529
|
scenario: row.scenario,
|
|
450
530
|
input: row.input,
|
|
531
|
+
traits: row.traits,
|
|
451
532
|
output: row.output,
|
|
452
533
|
status: row.status,
|
|
453
534
|
leaseUntil: row.lease_until,
|
|
@@ -476,7 +557,7 @@ class SqliteRunStore {
|
|
|
476
557
|
input TEXT NOT NULL, output TEXT, status TEXT NOT NULL,
|
|
477
558
|
lease_until TEXT NOT NULL, created_at TEXT NOT NULL,
|
|
478
559
|
error TEXT, request_key TEXT, inbox_ref TEXT,
|
|
479
|
-
presented_input TEXT, presented_output TEXT, error_code TEXT,
|
|
560
|
+
presented_input TEXT, presented_output TEXT, error_code TEXT, traits TEXT NOT NULL DEFAULT '{}',
|
|
480
561
|
UNIQUE(project, request_key)
|
|
481
562
|
)`);
|
|
482
563
|
const existing = new Set(this.db.query("PRAGMA table_info(runs)").all().map((column) => column.name));
|
|
@@ -484,12 +565,14 @@ class SqliteRunStore {
|
|
|
484
565
|
if (!existing.has(column))
|
|
485
566
|
this.db.exec(`ALTER TABLE runs ADD COLUMN ${column} TEXT`);
|
|
486
567
|
}
|
|
568
|
+
if (!existing.has("traits"))
|
|
569
|
+
this.db.exec("ALTER TABLE runs ADD COLUMN traits TEXT NOT NULL DEFAULT '{}'");
|
|
487
570
|
this.db.exec("CREATE INDEX IF NOT EXISTS runs_lease_idx ON runs(status, lease_until)");
|
|
488
571
|
}
|
|
489
572
|
async insert(run) {
|
|
490
|
-
const inserted = this.db.query(`INSERT INTO runs (id, project, scenario, input, output, status, lease_until, created_at, error, error_code, request_key, inbox_ref, presented_input, presented_output)
|
|
491
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
492
|
-
ON CONFLICT(project, request_key) DO NOTHING RETURNING id`).get(run.id, this.project, run.scenario, run.input, run.output, run.status, run.leaseUntil, run.createdAt, run.error, run.errorCode, run.requestKey, run.inboxRef, run.presentedInput, run.presentedOutput);
|
|
573
|
+
const inserted = this.db.query(`INSERT INTO runs (id, project, scenario, input, traits, output, status, lease_until, created_at, error, error_code, request_key, inbox_ref, presented_input, presented_output)
|
|
574
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
575
|
+
ON CONFLICT(project, request_key) DO NOTHING RETURNING id`).get(run.id, this.project, run.scenario, run.input, run.traits, run.output, run.status, run.leaseUntil, run.createdAt, run.error, run.errorCode, run.requestKey, run.inboxRef, run.presentedInput, run.presentedOutput);
|
|
493
576
|
return inserted !== null;
|
|
494
577
|
}
|
|
495
578
|
async find(id) {
|
package/package.json
CHANGED
package/types/index.d.ts
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import type { RunStatus, RunStore, StoredRun } from "./store.js";
|
|
2
2
|
import type { InboxAdapter } from "./remote-inbox.js";
|
|
3
3
|
import { type JsonSchema } from "./schema.js";
|
|
4
|
+
import { type TraitSpec, type TraitSummary, type Traits } from "./traits.js";
|
|
4
5
|
export type { ReadMessage } from "./extract.js";
|
|
5
6
|
export { validateInput } from "./schema.js";
|
|
6
7
|
export type { JsonSchema } from "./schema.js";
|
|
8
|
+
export { resolveTraits, summarizeTraits, buildState } from "./traits.js";
|
|
9
|
+
export type { TraitSpec, TraitSummary, TraitValue, TraitValues, Traits } from "./traits.js";
|
|
7
10
|
export { RemoteInbox, inboxAddress, inboxAlias, legacyInboxDomain } from "./remote-inbox.js";
|
|
8
11
|
export type { InboxAdapter, InboxMessage } from "./remote-inbox.js";
|
|
9
12
|
export type { RunPatch, RunStatus, RunStore, StoredRun } from "./store.js";
|
|
10
13
|
export type ScenarioContext<Input> = {
|
|
11
14
|
runId: string;
|
|
12
15
|
input: Input;
|
|
16
|
+
traits: Traits;
|
|
13
17
|
inbox?: {
|
|
14
18
|
address: (actor: string) => string;
|
|
15
19
|
};
|
|
@@ -23,6 +27,9 @@ export type Scenario<Input = unknown, Output = unknown> = {
|
|
|
23
27
|
description: string;
|
|
24
28
|
input?: JsonSchema;
|
|
25
29
|
actors?: ActorSpec[];
|
|
30
|
+
traits?: Record<string, TraitSpec<ScenarioContext<Input> & {
|
|
31
|
+
output: Output;
|
|
32
|
+
}>>;
|
|
26
33
|
prepare: (context: ScenarioContext<Input>) => Promise<Output>;
|
|
27
34
|
verify: (context: ScenarioContext<Input> & {
|
|
28
35
|
output: Output;
|
|
@@ -49,6 +56,7 @@ export type Run = {
|
|
|
49
56
|
project: string;
|
|
50
57
|
scenario: string;
|
|
51
58
|
input: unknown;
|
|
59
|
+
traits: Traits;
|
|
52
60
|
output: unknown | null;
|
|
53
61
|
status: RunStatus;
|
|
54
62
|
leaseUntil: string;
|
|
@@ -61,11 +69,17 @@ export type ErrorCode = "inbox_unavailable" | "prepare_failed" | "verify_failed"
|
|
|
61
69
|
export type ConditionOptions = {
|
|
62
70
|
now?: () => Date;
|
|
63
71
|
};
|
|
72
|
+
export type StartOptions = {
|
|
73
|
+
leaseMinutes?: number;
|
|
74
|
+
requestKey?: string;
|
|
75
|
+
traits?: Record<string, unknown>;
|
|
76
|
+
};
|
|
64
77
|
export type ScenarioSummary = {
|
|
65
78
|
name: string;
|
|
66
79
|
description: string;
|
|
67
80
|
input?: JsonSchema;
|
|
68
81
|
actors?: ActorSpec[];
|
|
82
|
+
traits?: Record<string, TraitSummary>;
|
|
69
83
|
};
|
|
70
84
|
export declare function describeProject(project: Project): {
|
|
71
85
|
project: string;
|
|
@@ -89,18 +103,12 @@ export declare class Condition {
|
|
|
89
103
|
private inboxFor;
|
|
90
104
|
private checkActor;
|
|
91
105
|
private active;
|
|
92
|
-
start(name: string, input?: unknown, options?: {
|
|
93
|
-
leaseMinutes?: number;
|
|
94
|
-
requestKey?: string;
|
|
95
|
-
}): Promise<{
|
|
106
|
+
start(name: string, input?: unknown, options?: StartOptions): Promise<{
|
|
96
107
|
run: Run;
|
|
97
108
|
created: boolean;
|
|
98
109
|
}>;
|
|
99
110
|
prepare(id: string): Promise<Run>;
|
|
100
|
-
condition(name: string, input?: unknown, options?:
|
|
101
|
-
leaseMinutes?: number;
|
|
102
|
-
requestKey?: string;
|
|
103
|
-
}): Promise<Run>;
|
|
111
|
+
condition(name: string, input?: unknown, options?: StartOptions): Promise<Run>;
|
|
104
112
|
login(id: string, actor: string): Promise<unknown>;
|
|
105
113
|
inbox(id: string, actor: string): Promise<{
|
|
106
114
|
messages: import("./extract").ReadMessage[];
|
package/types/store.d.ts
CHANGED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type TraitValue = string | number;
|
|
2
|
+
export type Traits = Record<string, TraitValue>;
|
|
3
|
+
export type TraitValues = readonly string[] | {
|
|
4
|
+
type: "integer";
|
|
5
|
+
minimum: number;
|
|
6
|
+
maximum: number;
|
|
7
|
+
};
|
|
8
|
+
export type TraitSummary = {
|
|
9
|
+
description?: string;
|
|
10
|
+
values: TraitValues;
|
|
11
|
+
default?: TraitValue;
|
|
12
|
+
requires?: Record<string, Record<string, readonly TraitValue[]>>;
|
|
13
|
+
};
|
|
14
|
+
export type TraitSpec<Context> = TraitSummary & {
|
|
15
|
+
apply: (context: Context & {
|
|
16
|
+
value: TraitValue;
|
|
17
|
+
}) => Promise<unknown>;
|
|
18
|
+
};
|
|
19
|
+
export declare function resolveTraits(specs: Record<string, TraitSummary> | undefined, requested?: unknown): Traits;
|
|
20
|
+
export declare function summarizeTraits(specs: Record<string, TraitSummary>): Record<string, TraitSummary>;
|
|
21
|
+
export declare function buildState<Context extends {
|
|
22
|
+
traits: Traits;
|
|
23
|
+
}>(prepare: (context: Context) => Promise<unknown>, specs: Record<string, TraitSpec<Context & {
|
|
24
|
+
output: unknown;
|
|
25
|
+
}>> | undefined, context: Context): Promise<unknown>;
|