@volter/world-core 3.0.166 → 3.0.167
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/dist/src/changeset.d.ts +9 -0
- package/dist/src/changeset.js +5 -2
- package/dist/src/completeness.d.ts +36 -0
- package/dist/src/completeness.js +46 -0
- package/dist/src/derived-core.d.ts +27 -0
- package/dist/src/derived-core.js +31 -2
- package/dist/src/events.d.ts +6 -1
- package/dist/src/events.js +58 -13
- package/dist/src/index.d.ts +4 -2
- package/dist/src/index.js +3 -2
- package/dist/src/openai-wire.js +0 -1
- package/dist/src/pack-fetch.d.ts +1 -3
- package/dist/src/pack-fetch.js +103 -43
- package/dist/src/runtime.d.ts +5 -2
- package/dist/src/runtime.js +3 -1
- package/dist/src/scenario-consume.d.ts +43 -0
- package/dist/src/scenario-consume.js +111 -0
- package/dist/src/scenario-serve.d.ts +16 -0
- package/dist/src/scenario-serve.js +47 -0
- package/dist/src/scenario-timeline.d.ts +131 -0
- package/dist/src/scenario-timeline.js +242 -0
- package/dist/src/scenario.d.ts +15 -93
- package/dist/src/scenario.js +34 -323
- package/dist/src/sockets.d.ts +8 -4
- package/dist/src/sockets.js +1 -1
- package/dist/src/world-clock.d.ts +3 -0
- package/dist/src/world-clock.js +14 -9
- package/package.json +1 -1
- package/src/changeset.ts +13 -3
- package/src/completeness.ts +57 -0
- package/src/derived-core.ts +41 -2
- package/src/events.ts +58 -11
- package/src/index.ts +5 -3
- package/src/openai-wire.ts +0 -1
- package/src/pack-fetch.ts +81 -39
- package/src/runtime.ts +5 -2
- package/src/scenario-consume.ts +123 -0
- package/src/scenario-serve.ts +47 -0
- package/src/scenario-timeline.ts +269 -0
- package/src/scenario.ts +43 -341
- package/src/sockets.ts +5 -5
- package/src/world-clock.ts +16 -9
package/dist/src/pack-fetch.js
CHANGED
|
@@ -13,11 +13,16 @@ import { ORIGINAL_PATH_HEADER } from "./sigv4.js";
|
|
|
13
13
|
import { placedCookies, TWIN_PREFIX_HEADER } from "./twin-fetch.js";
|
|
14
14
|
import { PREFIX_PARAMS_HEADER } from "./derived.js";
|
|
15
15
|
import { withHeaders } from "./with-headers.js";
|
|
16
|
-
import { authRefusal, bindSemantics, contextFor, LANE_HEADER, SCENARIO_DECISION_HEADER, scenarioDecisionHeader, storedType, coreFor, crossCutting, derivedRequestScopes, vendorError } from "./derived-core.js";
|
|
16
|
+
import { authRefusal, bindSemantics, contextFor, scenarioLedgerOf, scenarioRecords, LANE_HEADER, SCENARIO_DECISION_HEADER, scenarioDecisionHeader, storedType, coreFor, crossCutting, derivedRequestScopes, vendorError } from "./derived-core.js";
|
|
17
17
|
import { compileSurface, createDerivedFetch, matchOperation, pathParams } from "./derived.js";
|
|
18
18
|
import { isReadOnlyRequest, READ_ONLY_REQUEST_HEADER, runAsVendorMove } from "./request-scope.js";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
19
|
+
import { ScenarioError, statefulTwinManifest, twinManifest } from "./scenario.js";
|
|
20
|
+
import { placedOn } from "./scenario-timeline.js";
|
|
21
|
+
import { rateLimited, recordFault, requestFaults } from "./scenario-consume.js";
|
|
22
|
+
import { faultAnswer, kernelFault, serveTurn } from "./scenario-serve.js";
|
|
23
|
+
import { onHeldDelivery, releaseHeldDeliveries } from "./events.js";
|
|
24
|
+
import { getActiveWorldStore, withWorldStore } from "./world-store.js";
|
|
25
|
+
import { noteCompleteness } from "./completeness.js";
|
|
21
26
|
import { runWithCorrelationId } from "./actions.js";
|
|
22
27
|
import { deferPerform, deployableEntries, performRequestGroup, RefusedWriteError, VendorWriteError } from "./head.js";
|
|
23
28
|
import { FLUSH, ZERO_SHA, pktLine, readPktLines } from "./git/smart-http.js";
|
|
@@ -30,7 +35,7 @@ import { expireSocketWaits, socketUpgrade } from "./sockets.js";
|
|
|
30
35
|
import { liveIngest } from "./live/ingest.js";
|
|
31
36
|
import { readResourceBlob, removeResourceBlob, writeResourceBlob } from "./resource-blob.js";
|
|
32
37
|
import { gitRepositoryOf, resourcesOfType } from "./derived-core.js";
|
|
33
|
-
import { worldNow } from "./world-clock.js";
|
|
38
|
+
import { worldClockRunning, worldNow } from "./world-clock.js";
|
|
34
39
|
import { MACHINE_POOL, POOL_KINDS, resetFakePool } from "./machines.js";
|
|
35
40
|
import { applyTwinWrite } from "./serve.js";
|
|
36
41
|
import { graphqlEndpoint } from "./graphql-wire.js";
|
|
@@ -260,6 +265,8 @@ export function createPackFetch(parts, options = {}) {
|
|
|
260
265
|
const refused = m.auth?.beforeRouting ? authRefusal(m, request, options.root) : undefined;
|
|
261
266
|
if (refused)
|
|
262
267
|
return refused;
|
|
268
|
+
// an operation this twin does not implement is what the World is missing at this vendor (ADR 0036 §9)
|
|
269
|
+
noteCompleteness(process.env, { kind: 'unimplemented', vendor: m.vendor, method: request.method, path: new URL(request.url).pathname, ...(operation ? { operation: operation.id } : {}) });
|
|
263
270
|
if (parts.gap) {
|
|
264
271
|
const url = new URL(request.url);
|
|
265
272
|
const matched = operation ? matchOperation(routes, request.method, url.pathname, url.searchParams, request.headers) : undefined;
|
|
@@ -284,25 +291,39 @@ export function createPackFetch(parts, options = {}) {
|
|
|
284
291
|
return graphql(request);
|
|
285
292
|
return rest(request);
|
|
286
293
|
}, { owners: rest.owners });
|
|
287
|
-
// the World's scenario, read
|
|
288
|
-
//
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
if (
|
|
298
|
-
|
|
299
|
-
|
|
294
|
+
// the World's scenario (scenario-timeline.ts), read through the World's store at every request as the clock is: a
|
|
295
|
+
// model vendor's turns, and the faults placed on this vendor's requests, consumed and recorded in its own log
|
|
296
|
+
const placesThis = () => placedOn(m.vendor).length > 0;
|
|
297
|
+
// the work the World's time makes due (ADR 0036 §4): done when the clock's mover asks (`POST /twin/due`) and waits, and
|
|
298
|
+
// under a running clock by one timer set for the instant the next work falls due
|
|
299
|
+
let dueTimer;
|
|
300
|
+
const due = async () => {
|
|
301
|
+
if (!m.events)
|
|
302
|
+
return null;
|
|
303
|
+
const next = await releaseHeldDeliveries(m.service, options.root, m.events.record);
|
|
304
|
+
if (dueTimer !== undefined) {
|
|
305
|
+
clearTimeout(dueTimer);
|
|
306
|
+
dueTimer = undefined;
|
|
307
|
+
}
|
|
308
|
+
const store = getActiveWorldStore();
|
|
309
|
+
if (next !== null && worldClockRunning()) {
|
|
310
|
+
dueTimer = setTimeout(() => { dueTimer = undefined; void withWorldStore(store, due).catch(() => undefined); }, Math.max(0, Date.parse(next) - Date.parse(worldNow())));
|
|
311
|
+
dueTimer.unref?.();
|
|
300
312
|
}
|
|
301
|
-
return
|
|
313
|
+
return next;
|
|
302
314
|
};
|
|
303
|
-
|
|
315
|
+
// a packet held under a running clock sets the timer for when it falls due; so does a twin that starts with held work
|
|
316
|
+
// in its store (what time crossed while it was down is sent at once)
|
|
317
|
+
const arm = () => { if (worldClockRunning())
|
|
318
|
+
void due().catch(() => undefined); };
|
|
319
|
+
if (m.events) {
|
|
320
|
+
onHeldDelivery(m.service, options.root, arm);
|
|
321
|
+
const store = getActiveWorldStore();
|
|
322
|
+
queueMicrotask(() => { void withWorldStore(store, async () => arm()); });
|
|
323
|
+
}
|
|
324
|
+
/** The API, a turn handed its decision (ctx.scenario), or answered as the fault the World's scenario placed on it. */
|
|
304
325
|
const decided = async (arrived) => {
|
|
305
|
-
if (!parts.scenario)
|
|
326
|
+
if (!parts.scenario && !placesThis())
|
|
306
327
|
return api(arrived);
|
|
307
328
|
// the body is read once and the request made again from its bytes, so the turn's context and the API each read their
|
|
308
329
|
// own. Merge served with --scenario once answered no turn in 8 s (t_c0279b69), and this change came with the cure;
|
|
@@ -313,32 +334,69 @@ export function createPackFetch(parts, options = {}) {
|
|
|
313
334
|
const request = new Request(arrived.url, { method: arrived.method, headers: arrived.headers, signal: arrived.signal, ...(bytes ? { body: bytes } : {}) });
|
|
314
335
|
const url = new URL(request.url);
|
|
315
336
|
const matched = matchOperation(routes, request.method, url.pathname, url.searchParams, request.headers);
|
|
316
|
-
if (!matched
|
|
337
|
+
if (!matched)
|
|
317
338
|
return api(request);
|
|
318
|
-
|
|
319
|
-
if (!
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
339
|
+
const turn = parts.scenario?.operations.includes(matched.operation.id) === true;
|
|
340
|
+
if (!turn && !placesThis())
|
|
341
|
+
return api(request);
|
|
342
|
+
const ctx = await contextFor(m, { request: request.clone(), operation: matched.operation, params: matched.params }, scope);
|
|
343
|
+
const ledger = scenarioLedgerOf(ctx);
|
|
344
|
+
const asked = turn ? parts.scenario.request(ctx) : undefined;
|
|
345
|
+
/** A fault's answer in the vendor's own envelope: the model wire's, where the pack renders a turn's, else the manifest's
|
|
346
|
+
* error; null when it holds the request (`slow`) and the answer follows. */
|
|
347
|
+
const answerFault = async (fault) => {
|
|
348
|
+
const placed = kernelFault(fault);
|
|
349
|
+
const adapter = parts.scenario?.adapter;
|
|
350
|
+
if (placed.kind !== 'status' || (asked !== undefined && adapter?.renderFault))
|
|
351
|
+
return faultAnswer(fault, asked !== undefined && adapter?.renderFault ? () => adapter.renderFault(placed, asked) : undefined, request.signal);
|
|
352
|
+
const refused = vendorError(m, { status: placed.status, code: placed.errorType ?? 'twin_fault', message: placed.message });
|
|
353
|
+
if (placed.retryAfterSeconds !== undefined)
|
|
354
|
+
refused.headers.set('retry-after', String(placed.retryAfterSeconds));
|
|
355
|
+
return refused;
|
|
356
|
+
};
|
|
325
357
|
try {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
358
|
+
// a rejection at the vendor's edge comes before its credential is read; a rate limit counts every request
|
|
359
|
+
const fault = requestFaults(ledger).find((f) => f.on === undefined && f.point === undefined && (f.operation === undefined || f.operation === matched.operation.id));
|
|
360
|
+
if (fault?.kind === 'rejected') {
|
|
361
|
+
await recordFault(ledger, fault, { operation: matched.operation.id });
|
|
362
|
+
return (await answerFault(fault));
|
|
363
|
+
}
|
|
364
|
+
const limited = await rateLimited(ledger, matched.operation.id);
|
|
365
|
+
if (limited)
|
|
366
|
+
return (await answerFault(limited));
|
|
367
|
+
// the credential is the vendor's first question: a request it refuses takes no turn and meets no other fault
|
|
368
|
+
if ((turn || fault) && !m.auth?.exempt?.includes(matched.operation.id)) {
|
|
369
|
+
const refused = authRefusal(m, request, options.root);
|
|
370
|
+
if (refused)
|
|
371
|
+
return refused;
|
|
372
|
+
}
|
|
373
|
+
if (fault) {
|
|
374
|
+
await recordFault(ledger, fault, { operation: matched.operation.id });
|
|
375
|
+
// committed, and its answer lost: the write lands, and the app is told it failed
|
|
376
|
+
if (fault.kind === 'lost') {
|
|
377
|
+
await (await answer(request, asked, ledger)).arrayBuffer();
|
|
378
|
+
return (await answerFault({ ...fault, kind: 'failed' }));
|
|
379
|
+
}
|
|
380
|
+
const answered = await answerFault(fault);
|
|
381
|
+
if (answered)
|
|
382
|
+
return answered;
|
|
383
|
+
}
|
|
384
|
+
return await answer(request, asked, ledger);
|
|
333
385
|
}
|
|
334
386
|
catch (error) {
|
|
335
387
|
if (!(error instanceof ScenarioError))
|
|
336
388
|
throw error;
|
|
337
389
|
return vendorError(m, { status: 500, code: 'scenario_error', message: `the World's scenario: ${error.message}` });
|
|
338
390
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
391
|
+
};
|
|
392
|
+
/** The request answered by the API, a turn first decided by the World's scenario. */
|
|
393
|
+
const answer = async (request, asked, ledger) => {
|
|
394
|
+
if (asked === undefined)
|
|
395
|
+
return api(request);
|
|
396
|
+
const served = await serveTurn(asked, ledger, parts.scenario.adapter, request.signal);
|
|
397
|
+
if (served.kind === 'fault')
|
|
398
|
+
return Response.json(served.result.body, { status: served.result.status, headers: served.result.headers ?? {} });
|
|
399
|
+
return api(withHeaders(request, new Headers([...request.headers, [SCENARIO_DECISION_HEADER, scenarioDecisionHeader(served)]])));
|
|
342
400
|
};
|
|
343
401
|
for (const d of m.doors ?? [])
|
|
344
402
|
if (!parts.doors?.[d.id])
|
|
@@ -513,18 +571,20 @@ export function createPackFetch(parts, options = {}) {
|
|
|
513
571
|
const body = twinManifest({
|
|
514
572
|
vendor: m.vendor, twinOf: d.twinOf,
|
|
515
573
|
stateSentence: `Stateful: it stores ${d.stores}. Create state through the vendor's OWN API with the real SDK or plain fetch pointed here${discoveryDoors.length ? "; what the vendor does on its own pages (not its API) is a door of the World, listed under doors" : ''}.`,
|
|
516
|
-
behaviorSentence: d.behavior, exampleHandler: d.exampleHandler ?? null,
|
|
574
|
+
behaviorSentence: d.behavior, exampleHandler: d.exampleHandler ?? null,
|
|
517
575
|
});
|
|
518
576
|
const declared = Object.fromEntries(discoveryDoors.map((door) => [`${door.method} ${doorPath(door.path)}`, door.note ?? 'a door of the World']));
|
|
519
577
|
return Response.json({ ...shown, ...body, program: { ...body.program, ...(identity ? { identity } : {}) }, doors: { ...body.doors, ...declared } });
|
|
520
578
|
}
|
|
521
579
|
return Response.json({ ...statefulTwinManifest({ vendor: m.vendor, twinOf: d.twinOf, stores: d.stores, ...(identity ? { identity } : {}), ...(d.behavior ? { behavior: d.behavior } : {}), ...(d.notes ? { notes: d.notes } : {}), ...(discoveryDoors.length ? { doors: discoveryDoors.map((door) => ({ ...door, path: doorPath(door.path) })) } : {}) }), ...shown });
|
|
522
580
|
}
|
|
523
|
-
// the
|
|
581
|
+
// the World's clock has moved: do what it made due, and answer once it is done (ADR 0036 §4)
|
|
582
|
+
if (request.method === 'POST' && path === '/twin/due')
|
|
583
|
+
return Response.json({ next: await due() });
|
|
584
|
+
// what the World's scenario placed on this vendor, and what the twin consumed of it (its `_decision` and `_fault` records)
|
|
524
585
|
if (request.method === 'GET' && path === '/twin/scenario') {
|
|
525
586
|
const lanes = await laneDoor(request, '/twin/scenario');
|
|
526
|
-
|
|
527
|
-
return Response.json({ ...(engineOf()?.status() ?? { vendor: m.vendor, handlers: [], misses: 0, recentMisses: [] }), ...(Object.keys(lanes).length ? { lanes } : {}) });
|
|
587
|
+
return Response.json({ ...scenarioRecords(m, options.root), ...(Object.keys(lanes).length ? { lanes } : {}) });
|
|
528
588
|
}
|
|
529
589
|
// the vendor's own moves, not the caller's: its context carries none of the request's headers (a Stripe-Account
|
|
530
590
|
// header would make a payout time makes the caller's), but a tenant's, when the vendor keeps tenants apart
|
|
@@ -732,7 +792,7 @@ export function createPackFetch(parts, options = {}) {
|
|
|
732
792
|
throw new Error(`${m.vendor}: the manifest declares the socket ${d.id}, and semantics/sockets.ts exports no ${d.id}`);
|
|
733
793
|
const ownUpgrade = m.sockets?.length ? socketUpgrade(m.service, options.root, m.sockets, parts.sockets, (request, d, params) => open(request, `socket:${d.id}`, 'GET', d.path, params), hostOf, (request) => new URL(routed(request).url).pathname,
|
|
734
794
|
// a turn a session speaks (a voice agent's reply) is the World's scenario's, as a turn over HTTP is
|
|
735
|
-
async (asked) =>
|
|
795
|
+
async (asked, context) => (parts.scenario ? serveTurn(asked, scenarioLedgerOf(await context()), parts.scenario.adapter) : undefined)) : undefined;
|
|
736
796
|
const rawUpgrade = laneUpgrades(parts.lanes ?? {}, ownUpgrade);
|
|
737
797
|
const opening = new WeakMap();
|
|
738
798
|
const upgrade = rawUpgrade && m.tenant?.storage === 'namespaced' && parts.tenant ? {
|
package/dist/src/runtime.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export { clearRegistry, getPack, hasPack, listPacks, packOf, registerPack, pullPosture, assertContinuousPullAllowed, pullOnSchedule, resolvePullVendor, DEFAULT_PULL_POSTURE, DEFAULT_PULL_TRIGGER, PROTOCOL_VERSION, PROTOCOL_MAJOR, PACK_ARCHETYPES, protocolStanding } from './packRegistry.js';
|
|
2
2
|
export type { PackTransport, PullPosture, PullTrigger, PullVendor, RoundTripWrite, TwinPack } from './packRegistry.js';
|
|
3
|
-
export {
|
|
3
|
+
export { scenarioFaultResult, ScenarioError, statefulTwinManifest, twinManifest } from './scenario.js';
|
|
4
|
+
export { durationMs, MACHINE_LAYERS, parseScenario, resolveScenario, ScenarioTimelineError } from './scenario-timeline.js';
|
|
5
|
+
export type { ResolvedScenario, ScenarioAction, ScenarioCall, ScenarioFaultInput } from './scenario-timeline.js';
|
|
6
|
+
export { completenessMark, completenessSince, noteCompleteness, type Completeness } from './completeness.js';
|
|
4
7
|
export { WORLD_CLOCK_ENV, worldNow } from './world-clock.js';
|
|
5
8
|
export { WORLD_ENV_NAMES_ENV, worldEnvValue } from './world-env.js';
|
|
6
9
|
export { ORIGINAL_PATH_HEADER } from './sigv4.js';
|
|
@@ -19,7 +22,7 @@ export type { EventDelivery, EventRender, EventScheme, EventsDecl, EventTranspor
|
|
|
19
22
|
export type { Actor, CoreScope, DerivedManifest, ErrorSpec, FieldRule, Handler, HandlerContext, ResourceDecl, WriteHookContext, ScreenDecl, Semantics, SemanticsContext, StateField, Transition, TransitionObserver, SeedCall } from './derived-core.js';
|
|
20
23
|
export type { RemoteExecute, RemoteExecuteRequest, RemoteExecuteResponse } from './remote-execute.js';
|
|
21
24
|
export type { TwinFetchAdapterConfig, TwinFetchHandlerRequest, TwinFetchHandlerResult, TwinStream, TwinStreamConnection, TwinStreamSink } from './twin-fetch.js';
|
|
22
|
-
export type { PackScenarioAdapter, ScenarioDecision,
|
|
25
|
+
export type { PackScenarioAdapter, ScenarioDecision, ScenarioFault, ScenarioFaultResult, ScenarioFeatures, ScenarioHandler, ScenarioMatcher, ScenarioMissRecord } from './scenario.js';
|
|
23
26
|
export { emitTwinEvent, eventSubscriptionMatches, listEmittable, runEmitCli } from './emit.js';
|
|
24
27
|
export type { EmitDeliveryResult, EmitEndpoint, EmitReport, EmittableEvent, SynthesizedDelivery, TwinEmitter } from './emit.js';
|
|
25
28
|
export { DEFAULT_RATE_BUDGET, MAX_RATE_BUDGET_CEILING, MAX_RATE_BUDGET_WINDOW_MS, MIN_RATE_BUDGET_WEIGHT, MIN_RATE_BUDGET_WINDOW_MS, RateBudget, RateBudgetError, declareRateBudget, hasRateBudgetDeclaration, listRateBudgets, priceCall, rateBudgetPath, rateBudgetPolicy, rateBudgetWeight, resetToSeconds, assertBudgetGuardIntact, } from './rateBudget.js';
|
package/dist/src/runtime.js
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
export { clearRegistry, getPack, hasPack, listPacks, packOf, registerPack, pullPosture, assertContinuousPullAllowed, pullOnSchedule, resolvePullVendor, DEFAULT_PULL_POSTURE, DEFAULT_PULL_TRIGGER, PROTOCOL_VERSION, PROTOCOL_MAJOR, PACK_ARCHETYPES, protocolStanding } from "./packRegistry.js";
|
|
9
9
|
// The DELIVER verb (`emit`) — vendor-agnostic engine + CLI glue; packs declare a TwinEmitter
|
|
10
10
|
// (their event catalog, endpoints-from-state, and signed synthesis) on `TwinPack.emitter`.
|
|
11
|
-
export {
|
|
11
|
+
export { scenarioFaultResult, ScenarioError, statefulTwinManifest, twinManifest } from "./scenario.js";
|
|
12
|
+
export { durationMs, MACHINE_LAYERS, parseScenario, resolveScenario, ScenarioTimelineError } from "./scenario-timeline.js";
|
|
13
|
+
export { completenessMark, completenessSince, noteCompleteness } from "./completeness.js";
|
|
12
14
|
export { WORLD_CLOCK_ENV, worldNow } from "./world-clock.js";
|
|
13
15
|
export { WORLD_ENV_NAMES_ENV, worldEnvValue } from "./world-env.js";
|
|
14
16
|
export { ORIGINAL_PATH_HEADER } from "./sigv4.js";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type PlacedDecision, type PlacedFault } from './scenario-timeline.js';
|
|
2
|
+
/** What a consumption reads and writes: the twin's own bookkeeping rows, at the request's World time. `recording` is
|
|
3
|
+
* false for a read-only request, which consumes nothing. */
|
|
4
|
+
export type ScenarioLedger = {
|
|
5
|
+
vendor: string;
|
|
6
|
+
service: string;
|
|
7
|
+
root?: string;
|
|
8
|
+
at: string;
|
|
9
|
+
recording: boolean;
|
|
10
|
+
rows(type: '_decision' | '_fault' | '_fact' | '_rate'): Array<Record<string, unknown>>;
|
|
11
|
+
record(type: '_decision' | '_fault' | '_fact' | '_rate', fields: Record<string, unknown>, id?: string): Promise<unknown>;
|
|
12
|
+
};
|
|
13
|
+
/** The vendor's judgement at `point` for `subject`: the first decision the timeline placed for it, else `byDefault`;
|
|
14
|
+
* recorded either way. */
|
|
15
|
+
export declare function decide<T>(ledger: ScenarioLedger, point: string, subject: string, byDefault: T): Promise<T>;
|
|
16
|
+
/** The model turns the timeline placed for this vendor and not yet used up, in order: what the kernel matches a turn
|
|
17
|
+
* against with the pack's matchers. */
|
|
18
|
+
export declare function turnDecisions(ledger: ScenarioLedger): PlacedDecision[];
|
|
19
|
+
/** Record a turn's decision: the placed one that answered it, or the labelled default. */
|
|
20
|
+
export declare function recordTurn(ledger: ScenarioLedger, chosen: PlacedDecision | undefined, outcome: unknown): Promise<void>;
|
|
21
|
+
/** The request faults placed on this vendor that are not used up, in order. */
|
|
22
|
+
export declare function requestFaults(ledger: ScenarioLedger): PlacedFault[];
|
|
23
|
+
/** Record a fault that landed. */
|
|
24
|
+
export declare function recordFault(ledger: ScenarioLedger, fault: PlacedFault, where: Record<string, unknown>): Promise<void>;
|
|
25
|
+
/** A fault the timeline placed at a point of the pack's own (one item of a batch), for `subject`; recorded when it lands. */
|
|
26
|
+
export declare function faultAt(ledger: ScenarioLedger, point: string, subject: string): Promise<PlacedFault | undefined>;
|
|
27
|
+
/** A ledger over a twin's store alone, for the kernel's own paths that open no context (an event's delivery, a list the
|
|
28
|
+
* core answers). */
|
|
29
|
+
export declare function serviceLedger(input: {
|
|
30
|
+
vendor: string;
|
|
31
|
+
service: string;
|
|
32
|
+
root?: string;
|
|
33
|
+
at: string;
|
|
34
|
+
recording: boolean;
|
|
35
|
+
}): ScenarioLedger;
|
|
36
|
+
/** The first webhook fault placed on this vendor for an event type, not used up. */
|
|
37
|
+
export declare function webhookFault(ledger: ScenarioLedger, type: string): PlacedFault | undefined;
|
|
38
|
+
/** The `lag` the timeline placed on this vendor's lists (an `operation` names one list), if any. */
|
|
39
|
+
export declare function listLag(ledger: ScenarioLedger, operation: string): PlacedFault | undefined;
|
|
40
|
+
/** A rate limit the timeline placed, counted: this request's count in its window, and whether it is past the limit. */
|
|
41
|
+
export declare function rateLimited(ledger: ScenarioLedger, operation: string): Promise<PlacedFault | undefined>;
|
|
42
|
+
/** A World fact the twin answers with, recorded as answered (undefined when the World holds none). */
|
|
43
|
+
export declare function fact(ledger: ScenarioLedger, kind: string, key: string): Promise<unknown>;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// How a twin consumes its World's scenario (architecture, "Scenarios"; ADR 0036 §2-4, §10): a decision where its vendor
|
|
2
|
+
// decides, a fault where it lands, a World fact where the vendor answers with one. Every consumption is recorded in the
|
|
3
|
+
// twin's own log as bookkeeping (`_decision`, `_fault`, `_fact`), so the state a run reaches holds what was decided and
|
|
4
|
+
// what failed, a `once` decision or a counted fault is used up by the log rather than by a process's memory, and a branch
|
|
5
|
+
// or a replay agrees. A twin whose head is real consults no scenario: its vendor decides and fails on its own.
|
|
6
|
+
import { headOf } from "./head.js";
|
|
7
|
+
import { applyTwinWriteAtomic, twinResources } from "./serve.js";
|
|
8
|
+
import { durationMs, placedDecisions, placedFaults, worldJsonFile } from "./scenario-timeline.js";
|
|
9
|
+
const WORLD_FACTS_ENV = 'TWIN_WORLD_FACTS_FILE';
|
|
10
|
+
const simulated = (ledger) => headOf(ledger.service, ledger.root).kind !== 'real';
|
|
11
|
+
const usedTimes = (ledger, type, index) => ledger.rows(type).filter((r) => r.entry === index).length;
|
|
12
|
+
const available = (ledger, type, index, limit) => limit === undefined || usedTimes(ledger, type, index) < limit;
|
|
13
|
+
const decisionLeft = (ledger, d) => available(ledger, '_decision', d.index, d.once ? 1 : undefined);
|
|
14
|
+
const faultLeft = (ledger, f) => available(ledger, '_fault', f.index, f.times);
|
|
15
|
+
/** The vendor's judgement at `point` for `subject`: the first decision the timeline placed for it, else `byDefault`;
|
|
16
|
+
* recorded either way. */
|
|
17
|
+
export async function decide(ledger, point, subject, byDefault) {
|
|
18
|
+
if (!simulated(ledger))
|
|
19
|
+
return byDefault;
|
|
20
|
+
const chosen = placedDecisions(ledger.vendor, point, ledger.at).find((d) => d.on === undefined && (d.subject === undefined || d.subject === subject) && decisionLeft(ledger, d));
|
|
21
|
+
const outcome = (chosen ? chosen.outcome : byDefault);
|
|
22
|
+
if (ledger.recording)
|
|
23
|
+
await ledger.record('_decision', { point, subject, outcome, entry: chosen?.index ?? 'default', decidedAt: ledger.at });
|
|
24
|
+
return outcome;
|
|
25
|
+
}
|
|
26
|
+
/** The model turns the timeline placed for this vendor and not yet used up, in order: what the kernel matches a turn
|
|
27
|
+
* against with the pack's matchers. */
|
|
28
|
+
export function turnDecisions(ledger) {
|
|
29
|
+
if (!simulated(ledger))
|
|
30
|
+
return [];
|
|
31
|
+
return placedDecisions(ledger.vendor, 'turn', ledger.at).filter((d) => decisionLeft(ledger, d));
|
|
32
|
+
}
|
|
33
|
+
/** Record a turn's decision: the placed one that answered it, or the labelled default. */
|
|
34
|
+
export async function recordTurn(ledger, chosen, outcome) {
|
|
35
|
+
if (ledger.recording)
|
|
36
|
+
await ledger.record('_decision', { point: 'turn', outcome, entry: chosen?.index ?? 'default', decidedAt: ledger.at });
|
|
37
|
+
}
|
|
38
|
+
/** The request faults placed on this vendor that are not used up, in order. */
|
|
39
|
+
export function requestFaults(ledger) {
|
|
40
|
+
if (!simulated(ledger))
|
|
41
|
+
return [];
|
|
42
|
+
return placedFaults(ledger.vendor, 'request', ledger.at).filter((f) => faultLeft(ledger, f));
|
|
43
|
+
}
|
|
44
|
+
/** Record a fault that landed. */
|
|
45
|
+
export async function recordFault(ledger, fault, where) {
|
|
46
|
+
if (ledger.recording)
|
|
47
|
+
await ledger.record('_fault', { ...where, layer: fault.layer, kind: fault.kind, entry: fault.index, landedAt: ledger.at });
|
|
48
|
+
}
|
|
49
|
+
/** A fault the timeline placed at a point of the pack's own (one item of a batch), for `subject`; recorded when it lands. */
|
|
50
|
+
export async function faultAt(ledger, point, subject) {
|
|
51
|
+
const fault = requestFaults(ledger).find((f) => f.point === point && (f.subject === undefined || f.subject === subject));
|
|
52
|
+
if (fault)
|
|
53
|
+
await recordFault(ledger, fault, { point, subject });
|
|
54
|
+
return fault;
|
|
55
|
+
}
|
|
56
|
+
/** A ledger over a twin's store alone, for the kernel's own paths that open no context (an event's delivery, a list the
|
|
57
|
+
* core answers). */
|
|
58
|
+
export function serviceLedger(input) {
|
|
59
|
+
const rows = (type) => twinResources(input.service, input.root).filter((r) => r.type === type && r.deleted !== true);
|
|
60
|
+
return {
|
|
61
|
+
...input,
|
|
62
|
+
rows,
|
|
63
|
+
record: async (type, fields, id) => {
|
|
64
|
+
await applyTwinWriteAtomic(input.service, (resources) => {
|
|
65
|
+
const prefix = `${type.slice(1)}_`;
|
|
66
|
+
const next = id ?? `${prefix}${1 + Math.max(0, ...resources.filter((r) => r.type === type && r.id.startsWith(prefix)).map((r) => Number(r.id.slice(prefix.length)) || 0))}`;
|
|
67
|
+
return { kind: 'write', value: next, write: { operation: `${type.slice(1)}.record`, subjectType: type, subjectId: next, fields, occurredAt: input.at, actor: { kind: 'system' } } };
|
|
68
|
+
}, input.root);
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/** The first webhook fault placed on this vendor for an event type, not used up. */
|
|
73
|
+
export function webhookFault(ledger, type) {
|
|
74
|
+
return overTimeFaults(ledger).find((f) => f.kind.startsWith('webhook-') && (f.event === undefined || f.event === type) && faultLeft(ledger, f));
|
|
75
|
+
}
|
|
76
|
+
/** The `lag` the timeline placed on this vendor's lists (an `operation` names one list), if any. */
|
|
77
|
+
export function listLag(ledger, operation) {
|
|
78
|
+
return overTimeFaults(ledger).find((f) => f.kind === 'lag' && (f.operation === undefined || f.operation === operation));
|
|
79
|
+
}
|
|
80
|
+
/** The over-time faults placed on this vendor, in order. */
|
|
81
|
+
function overTimeFaults(ledger) {
|
|
82
|
+
if (headOf(ledger.service, ledger.root).kind === 'real')
|
|
83
|
+
return [];
|
|
84
|
+
return placedFaults(ledger.vendor, 'over-time', ledger.at);
|
|
85
|
+
}
|
|
86
|
+
/** A rate limit the timeline placed, counted: this request's count in its window, and whether it is past the limit. */
|
|
87
|
+
export async function rateLimited(ledger, operation) {
|
|
88
|
+
if (!ledger.recording)
|
|
89
|
+
return undefined;
|
|
90
|
+
for (const fault of overTimeFaults(ledger)) {
|
|
91
|
+
if (fault.kind !== 'rate-limit' || (fault.operation !== undefined && fault.operation !== operation))
|
|
92
|
+
continue;
|
|
93
|
+
const per = durationMs(fault.per);
|
|
94
|
+
const window = Math.floor((Date.parse(ledger.at) - Date.parse(fault.at)) / per);
|
|
95
|
+
const id = `rate_${fault.index}_${window}`;
|
|
96
|
+
const count = Number(ledger.rows('_rate').find((r) => r.id === id)?.count ?? 0) + 1;
|
|
97
|
+
await ledger.record('_rate', { count, entry: fault.index, window }, id);
|
|
98
|
+
if (count > fault.limit) {
|
|
99
|
+
await recordFault(ledger, fault, { operation, count });
|
|
100
|
+
return fault;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
/** A World fact the twin answers with, recorded as answered (undefined when the World holds none). */
|
|
106
|
+
export async function fact(ledger, kind, key) {
|
|
107
|
+
const value = worldJsonFile(WORLD_FACTS_ENV)?.[kind]?.[key];
|
|
108
|
+
if (ledger.recording)
|
|
109
|
+
await ledger.record('_fact', { kind, key, value: value ?? null, answeredAt: ledger.at });
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type PackScenarioAdapter, type ScenarioFault } from './scenario.js';
|
|
2
|
+
import { type ScenarioLedger } from './scenario-consume.js';
|
|
3
|
+
import type { PlacedFault } from './scenario-timeline.js';
|
|
4
|
+
import type { ScenarioServed } from './sockets.js';
|
|
5
|
+
/** A request fault as the kernel's fault grammar answers it: `rejected` at the edge (503 unless named), `failed` and
|
|
6
|
+
* `lost` as the vendor's server failure (500 unless named). */
|
|
7
|
+
export declare function kernelFault(fault: PlacedFault): ScenarioFault;
|
|
8
|
+
/** A model vendor's turn: a placed fault whose `on` the request satisfies lands first (a `slow` one holds the turn, then
|
|
9
|
+
* it is decided); then the first placed turn decision whose `on` it satisfies and whose answer it admits, else the
|
|
10
|
+
* miss, the pack's labelled stub. What landed and what decided are recorded either way. */
|
|
11
|
+
export declare function serveTurn<Req>(asked: Req, ledger: ScenarioLedger, adapter: PackScenarioAdapter<Req>, signal?: AbortSignal): Promise<ScenarioServed>;
|
|
12
|
+
/** What a request fault answers in place of the vendor's answer: the vendor's own envelope where the pack renders one. */
|
|
13
|
+
export declare function faultAnswer(fault: PlacedFault, render: (() => {
|
|
14
|
+
body: unknown;
|
|
15
|
+
headers?: Record<string, string>;
|
|
16
|
+
} | undefined) | undefined, signal?: AbortSignal): Promise<Response | null>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// A model vendor's turn and a request's fault, as the World's scenario places them (architecture, "Scenarios"). The
|
|
2
|
+
// placed turns are matched with the pack's own matchers (its `semantics/scenario.ts` adapter, through the kernel's
|
|
3
|
+
// grammar in scenario.ts), in timeline order; what answered, the labelled default or the fault that landed is recorded
|
|
4
|
+
// in the twin's log (scenario-consume.ts).
|
|
5
|
+
import { parseScenarioDocument, ScenarioEngine, scenarioFaultResult } from "./scenario.js";
|
|
6
|
+
import { recordFault, recordTurn, requestFaults, turnDecisions } from "./scenario-consume.js";
|
|
7
|
+
/** A request fault as the kernel's fault grammar answers it: `rejected` at the edge (503 unless named), `failed` and
|
|
8
|
+
* `lost` as the vendor's server failure (500 unless named). */
|
|
9
|
+
export function kernelFault(fault) {
|
|
10
|
+
if (fault.kind === 'slow')
|
|
11
|
+
return { kind: 'slow', ms: fault.ms ?? 0 };
|
|
12
|
+
if (fault.kind === 'drop')
|
|
13
|
+
return { kind: 'drop', ...(fault.ms !== undefined ? { holdMs: fault.ms } : {}) };
|
|
14
|
+
if (fault.kind === 'generation')
|
|
15
|
+
return { kind: 'generation', status: fault.status, message: fault.message, errorType: fault.errorType, ...(fault.metadata ? { metadata: fault.metadata } : {}), ...(fault.partialText !== undefined ? { partialText: fault.partialText } : {}), ...(fault.event !== undefined ? { event: fault.event } : {}) };
|
|
16
|
+
const status = fault.status ?? (fault.kind === 'rejected' ? 503 : fault.kind === 'rate-limit' ? 429 : 500);
|
|
17
|
+
return { kind: 'status', status, ...(fault.retryAfterSeconds !== undefined ? { retryAfterSeconds: fault.retryAfterSeconds } : {}), message: fault.message ?? `${fault.kind}: the World's scenario placed this fault (entry ${fault.index})`, ...(fault.errorType ? { errorType: fault.errorType } : {}), ...(fault.metadata ? { metadata: fault.metadata } : {}) };
|
|
18
|
+
}
|
|
19
|
+
/** A model vendor's turn: a placed fault whose `on` the request satisfies lands first (a `slow` one holds the turn, then
|
|
20
|
+
* it is decided); then the first placed turn decision whose `on` it satisfies and whose answer it admits, else the
|
|
21
|
+
* miss, the pack's labelled stub. What landed and what decided are recorded either way. */
|
|
22
|
+
export async function serveTurn(asked, ledger, adapter, signal) {
|
|
23
|
+
const engineOf = (handlers) => new ScenarioEngine(adapter, parseScenarioDocument({ handlers }, adapter));
|
|
24
|
+
const faults = requestFaults(ledger).filter((f) => f.on !== undefined);
|
|
25
|
+
if (faults.length) {
|
|
26
|
+
const hit = engineOf(faults.map((f) => ({ id: `entry-${f.index}`, on: f.on, fault: kernelFault(f) }))).next(asked);
|
|
27
|
+
if (hit.kind === 'handler') {
|
|
28
|
+
const fault = faults.find((f) => `entry-${f.index}` === hit.ruleId);
|
|
29
|
+
await recordFault(ledger, fault, { point: 'turn' });
|
|
30
|
+
// a generation failure is admitted, then fails: the pack's handler answers it in its SDK's error wire
|
|
31
|
+
if (fault.kind === 'generation')
|
|
32
|
+
return hit;
|
|
33
|
+
const result = await scenarioFaultResult(hit.fault, () => adapter.renderFault?.(hit.fault, asked), signal);
|
|
34
|
+
if (result !== null)
|
|
35
|
+
return { kind: 'fault', result, ruleId: hit.ruleId };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const decisions = turnDecisions(ledger);
|
|
39
|
+
const decided = engineOf(decisions.map((d) => ({ id: `entry-${d.index}`, on: d.on ?? {}, respond: d.outcome }))).next(asked);
|
|
40
|
+
await recordTurn(ledger, decided.kind === 'handler' ? decisions.find((d) => `entry-${d.index}` === decided.ruleId) : undefined, decided.kind === 'handler' ? decided.respond : null);
|
|
41
|
+
return decided;
|
|
42
|
+
}
|
|
43
|
+
/** What a request fault answers in place of the vendor's answer: the vendor's own envelope where the pack renders one. */
|
|
44
|
+
export async function faultAnswer(fault, render, signal) {
|
|
45
|
+
const result = await scenarioFaultResult(kernelFault(fault), render, signal);
|
|
46
|
+
return result === null ? null : Response.json(result.body, { status: result.status, headers: result.headers });
|
|
47
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
export declare const WORLD_SCENARIO_ENV = "TWIN_WORLD_SCENARIO_FILE";
|
|
2
|
+
/** A call to a vendor's API at its own URL, routed to its twin as the application's calls are; `${NAME}` in a header is
|
|
3
|
+
* the value the World sets for its application, and `as` names the person whose World browser carries its cookies. */
|
|
4
|
+
export type ScenarioCall = {
|
|
5
|
+
method: string;
|
|
6
|
+
url: string;
|
|
7
|
+
headers?: Record<string, string>;
|
|
8
|
+
body?: unknown;
|
|
9
|
+
as?: string;
|
|
10
|
+
};
|
|
11
|
+
export type ScenarioAction = {
|
|
12
|
+
person: string;
|
|
13
|
+
browser?: string;
|
|
14
|
+
run: string;
|
|
15
|
+
};
|
|
16
|
+
/** The outcome of a vendor's judgement at one of its decision points, for the subject it is keyed to (absent: every
|
|
17
|
+
* subject); a model vendor's turn is the point `turn`, keyed by `on`, the pack's scenario matchers. */
|
|
18
|
+
type ScenarioDecisionInput = {
|
|
19
|
+
vendor: string;
|
|
20
|
+
point: string;
|
|
21
|
+
subject?: string;
|
|
22
|
+
on?: Record<string, unknown>;
|
|
23
|
+
outcome: unknown;
|
|
24
|
+
once?: boolean;
|
|
25
|
+
};
|
|
26
|
+
declare const FAULT_KINDS: {
|
|
27
|
+
readonly request: readonly ["rejected", "failed", "lost", "slow", "drop", "generation"];
|
|
28
|
+
readonly 'over-time': readonly ["webhook-delay", "webhook-duplicate", "webhook-drop", "lag", "rate-limit"];
|
|
29
|
+
readonly network: readonly ["reset", "dns", "tls", "latency", "loss", "partition"];
|
|
30
|
+
readonly infrastructure: readonly ["oom", "disk-full", "crash", "restart"];
|
|
31
|
+
readonly time: readonly ["jump"];
|
|
32
|
+
};
|
|
33
|
+
type FaultLayer = keyof typeof FAULT_KINDS;
|
|
34
|
+
/** The layers asked of the machine the app runs on, through its fault door (ADR 0036 §4). */
|
|
35
|
+
export declare const MACHINE_LAYERS: ReadonlyArray<FaultLayer>;
|
|
36
|
+
export type ScenarioFaultInput = {
|
|
37
|
+
layer: FaultLayer;
|
|
38
|
+
kind: string;
|
|
39
|
+
/** the twin a vendor-layer fault lands in */
|
|
40
|
+
vendor?: string;
|
|
41
|
+
/** a request fault's operation (the spec's operationId), or a model turn's `on`; neither: every request */
|
|
42
|
+
operation?: string;
|
|
43
|
+
on?: Record<string, unknown>;
|
|
44
|
+
/** a point of the pack's own (`ctx.fault(point, subject)`: one item of a batch) */
|
|
45
|
+
point?: string;
|
|
46
|
+
subject?: string;
|
|
47
|
+
status?: number;
|
|
48
|
+
message?: string;
|
|
49
|
+
errorType?: string;
|
|
50
|
+
metadata?: Record<string, unknown>;
|
|
51
|
+
partialText?: string;
|
|
52
|
+
event?: string;
|
|
53
|
+
retryAfterSeconds?: number;
|
|
54
|
+
ms?: number;
|
|
55
|
+
/** a delay, a lag or a window: `90s`, `5m`, `12h`, `30d` */
|
|
56
|
+
by?: string;
|
|
57
|
+
per?: string;
|
|
58
|
+
limit?: number;
|
|
59
|
+
/** a machine fault's part of the app, as the machine knows it; a reset's or a TLS failure's destination; a full disk's
|
|
60
|
+
* volume */
|
|
61
|
+
target?: string;
|
|
62
|
+
to?: string;
|
|
63
|
+
volume?: string;
|
|
64
|
+
between?: [string, string];
|
|
65
|
+
rate?: number;
|
|
66
|
+
/** how many times it lands (default: every time, until `until`) */
|
|
67
|
+
times?: number;
|
|
68
|
+
until?: string;
|
|
69
|
+
};
|
|
70
|
+
type ScenarioClock = {
|
|
71
|
+
advance: string;
|
|
72
|
+
} | {
|
|
73
|
+
set: string;
|
|
74
|
+
};
|
|
75
|
+
type TimelineEntry = {
|
|
76
|
+
at: string;
|
|
77
|
+
} & ({
|
|
78
|
+
call: ScenarioCall;
|
|
79
|
+
} | {
|
|
80
|
+
action: ScenarioAction;
|
|
81
|
+
} | {
|
|
82
|
+
decision: ScenarioDecisionInput;
|
|
83
|
+
} | {
|
|
84
|
+
fault: ScenarioFaultInput;
|
|
85
|
+
} | {
|
|
86
|
+
clock: ScenarioClock;
|
|
87
|
+
});
|
|
88
|
+
export type Scenario = {
|
|
89
|
+
start: string;
|
|
90
|
+
random?: string;
|
|
91
|
+
timeline: TimelineEntry[];
|
|
92
|
+
properties?: string;
|
|
93
|
+
};
|
|
94
|
+
/** A scenario as a World plays it: every `at` an instant, each entry its place in the timeline (what a twin's record of a
|
|
95
|
+
* consumption names). */
|
|
96
|
+
export type ResolvedScenario = {
|
|
97
|
+
name: string;
|
|
98
|
+
start: string;
|
|
99
|
+
random: string;
|
|
100
|
+
timeline: Array<TimelineEntry & {
|
|
101
|
+
index: number;
|
|
102
|
+
}>;
|
|
103
|
+
properties?: string;
|
|
104
|
+
};
|
|
105
|
+
export declare class ScenarioTimelineError extends Error {
|
|
106
|
+
}
|
|
107
|
+
/** `90s`, `5m`, `12h`, `30d` in milliseconds. */
|
|
108
|
+
export declare function durationMs(by: string): number;
|
|
109
|
+
/** Strict parse of a scenario document: every refusal names where it is and the vocabulary it expected. */
|
|
110
|
+
export declare function parseScenario(raw: unknown): Scenario;
|
|
111
|
+
/** Every `at` an instant: an instant as written, a duration after `start`. An entry before the one ahead of it, or before
|
|
112
|
+
* `start`, is refused: the clock moves only forward through a timeline. */
|
|
113
|
+
export declare function resolveScenario(name: string, doc: Scenario): ResolvedScenario;
|
|
114
|
+
/** A JSON file of the World named by an env variable (its scenario, its facts), read through the World's store at each
|
|
115
|
+
* call as the clock is, and parsed again only when it changed; null where the variable or the file is absent. */
|
|
116
|
+
export declare function worldJsonFile<T>(env: string): T | null;
|
|
117
|
+
/** The decisions and vendor faults the World's scenario places on one vendor, in timeline order. */
|
|
118
|
+
export declare function placedOn(vendor: string): ResolvedScenario['timeline'];
|
|
119
|
+
export type PlacedDecision = ScenarioDecisionInput & {
|
|
120
|
+
index: number;
|
|
121
|
+
at: string;
|
|
122
|
+
};
|
|
123
|
+
export type PlacedFault = ScenarioFaultInput & {
|
|
124
|
+
index: number;
|
|
125
|
+
at: string;
|
|
126
|
+
};
|
|
127
|
+
/** The decisions placed for a vendor's point at or before the World's now, in timeline order. */
|
|
128
|
+
export declare function placedDecisions(vendor: string, point: string, now?: string): PlacedDecision[];
|
|
129
|
+
/** The vendor faults placed on a vendor at or before the World's now and not past their `until`, in timeline order. */
|
|
130
|
+
export declare function placedFaults(vendor: string, layer: 'request' | 'over-time', now?: string): PlacedFault[];
|
|
131
|
+
export {};
|