@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.
Files changed (42) hide show
  1. package/dist/src/changeset.d.ts +9 -0
  2. package/dist/src/changeset.js +5 -2
  3. package/dist/src/completeness.d.ts +36 -0
  4. package/dist/src/completeness.js +46 -0
  5. package/dist/src/derived-core.d.ts +27 -0
  6. package/dist/src/derived-core.js +31 -2
  7. package/dist/src/events.d.ts +6 -1
  8. package/dist/src/events.js +58 -13
  9. package/dist/src/index.d.ts +4 -2
  10. package/dist/src/index.js +3 -2
  11. package/dist/src/openai-wire.js +0 -1
  12. package/dist/src/pack-fetch.d.ts +1 -3
  13. package/dist/src/pack-fetch.js +103 -43
  14. package/dist/src/runtime.d.ts +5 -2
  15. package/dist/src/runtime.js +3 -1
  16. package/dist/src/scenario-consume.d.ts +43 -0
  17. package/dist/src/scenario-consume.js +111 -0
  18. package/dist/src/scenario-serve.d.ts +16 -0
  19. package/dist/src/scenario-serve.js +47 -0
  20. package/dist/src/scenario-timeline.d.ts +131 -0
  21. package/dist/src/scenario-timeline.js +242 -0
  22. package/dist/src/scenario.d.ts +15 -93
  23. package/dist/src/scenario.js +34 -323
  24. package/dist/src/sockets.d.ts +8 -4
  25. package/dist/src/sockets.js +1 -1
  26. package/dist/src/world-clock.d.ts +3 -0
  27. package/dist/src/world-clock.js +14 -9
  28. package/package.json +1 -1
  29. package/src/changeset.ts +13 -3
  30. package/src/completeness.ts +57 -0
  31. package/src/derived-core.ts +41 -2
  32. package/src/events.ts +58 -11
  33. package/src/index.ts +5 -3
  34. package/src/openai-wire.ts +0 -1
  35. package/src/pack-fetch.ts +81 -39
  36. package/src/runtime.ts +5 -2
  37. package/src/scenario-consume.ts +123 -0
  38. package/src/scenario-serve.ts +47 -0
  39. package/src/scenario-timeline.ts +269 -0
  40. package/src/scenario.ts +43 -341
  41. package/src/sockets.ts +5 -5
  42. package/src/world-clock.ts +16 -9
@@ -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 { parseScenarioDocument, ScenarioEngine, ScenarioError, statefulTwinManifest, twinManifest } from "./scenario.js";
20
- import { getActiveWorldStore } from "./world-store.js";
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 from the World's store at every turn, HTTP or socket alike: a file the World rewrites takes
288
- // effect at the next turn, with no reboot; the engine (its handlers' counts) is kept while the file is unchanged
289
- let engine;
290
- let engineRaw;
291
- const engineOf = () => {
292
- if (!parts.scenario || options.scenarioPath === undefined)
293
- return undefined;
294
- const raw = getActiveWorldStore().read(options.scenarioPath);
295
- if (raw === null)
296
- throw new ScenarioError(`${m.vendor} scenario: no file at ${options.scenarioPath}`);
297
- if (!engine || raw !== engineRaw) {
298
- engine = new ScenarioEngine(parts.scenario.adapter, parseScenarioDocument(parseExactJson(raw), parts.scenario.adapter));
299
- engineRaw = raw;
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 engine;
313
+ return next;
302
314
  };
303
- /** The API, a turn the scenario decides handed its decision (ctx.scenario), or answered as the fault it names. */
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 || !parts.scenario.operations.includes(matched.operation.id))
337
+ if (!matched)
317
338
  return api(request);
318
- // the credential is the vendor's first question: a request it refuses takes no scripted turn
319
- if (!m.auth?.exempt?.includes(matched.operation.id)) {
320
- const refused = authRefusal(m, request, options.root);
321
- if (refused)
322
- return refused;
323
- }
324
- let decision;
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
- const turns = engineOf();
327
- if (!turns)
328
- return api(request);
329
- const asked = parts.scenario.request(await contextFor(m, { request: request.clone(), operation: matched.operation, params: matched.params }, scope));
330
- if (asked === undefined)
331
- return api(request);
332
- decision = await turns.serve(asked);
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
- if (decision.kind === 'fault')
340
- return Response.json(decision.result.body, { status: decision.result.status, headers: decision.result.headers ?? {} });
341
- return api(withHeaders(request, new Headers([...request.headers, [SCENARIO_DECISION_HEADER, scenarioDecisionHeader(decision)]])));
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, ...(engineOf() ? { engine: engineOf() } : {}),
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 scenario's handlers with their match counts, and the recent misses
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
- if (parts.scenario || Object.keys(lanes).length)
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) => engineOf()?.serve(asked)) : undefined;
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 ? {
@@ -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 { parseScenarioDocument, scenarioFaultResult, ScenarioEngine, ScenarioError, statefulTwinManifest, twinManifest } from './scenario.js';
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, ScenarioDocument, ScenarioExtractorSpec, ScenarioFault, ScenarioFaultResult, ScenarioFeatures, ScenarioHandler, ScenarioMatcher, ScenarioMissRecord, ScenarioStatus } from './scenario.js';
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';
@@ -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 { parseScenarioDocument, scenarioFaultResult, ScenarioEngine, ScenarioError, statefulTwinManifest, twinManifest } from "./scenario.js";
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 {};