@sublang/playbook 17.8.1 → 17.10.0
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/docs/configuration.md +29 -2
- package/package.json +2 -2
- package/reference/sdlc/captain.md +1 -1
- package/reference/sdlc/captain.playbook/captain.fsm.js +1 -1
- package/reference/sdlc/captain.playbook/captain.fsm.ts +1 -1
- package/reference/sdlc/captain.playbook/captain.gears.md +1 -1
- package/reference/sdlc/code.playbook/bin/launch-config.js +55 -13
- package/reference/sdlc/code.playbook/bin/repository-effects.js +26 -2
- package/reference/sdlc/code.playbook/bin/session-store.js +3 -0
- package/reference/sdlc/code.playbook/host-capabilities.d.ts +1 -0
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +2 -1
- package/reference/sdlc/code.playbook/playbook-captain.js +170 -48
- package/reference/sdlc/code.playbook/playbook-captain.ts +227 -63
- package/slc/link.md +25 -3
- package/src/runtime.d.ts +13 -0
- package/src/runtime.ts +14 -0
- package/src/xstate-playbook-runtime.d.ts +4 -3
- package/src/xstate-playbook-runtime.js +163 -19
- package/src/xstate-playbook-runtime.ts +197 -18
- package/src/xstate-runtime.d.ts +7 -5
- package/src/xstate-runtime.js +40 -9
- package/src/xstate-runtime.ts +53 -24
|
@@ -58,6 +58,7 @@ import type {
|
|
|
58
58
|
CaptainResult,
|
|
59
59
|
JsonValue,
|
|
60
60
|
PlaybookAdoptionContext,
|
|
61
|
+
PlaybookBossExchange,
|
|
61
62
|
PlaybookCallResult,
|
|
62
63
|
PlaybookControlAction,
|
|
63
64
|
PlaybookControlReceipt,
|
|
@@ -1327,6 +1328,9 @@ function pendingBossQuestionRecord(
|
|
|
1327
1328
|
const CONTINUATION_PREAMBLE =
|
|
1328
1329
|
'Continue the same task using Boss’s reply below.';
|
|
1329
1330
|
|
|
1331
|
+
// The labelled continuation a compiled artifact's composer renders, which
|
|
1332
|
+
// slc's prompt-contract verifier pins. The runtime no longer sends it: it
|
|
1333
|
+
// hands a composer no question and reply, and marks them itself (DR-090 §2).
|
|
1330
1334
|
function continuationBlocks(input: {
|
|
1331
1335
|
pendingBossQuestion?: { readonly question: string };
|
|
1332
1336
|
bossReply?: string;
|
|
@@ -1341,6 +1345,76 @@ function continuationBlocks(input: {
|
|
|
1341
1345
|
];
|
|
1342
1346
|
}
|
|
1343
1347
|
|
|
1348
|
+
// DR-090 §2: a Boss exchange reaches a player as structure, not as an
|
|
1349
|
+
// English instruction — a marker line before each question and each reply,
|
|
1350
|
+
// whatever language the run speaks.
|
|
1351
|
+
const BOSS_QUESTION_MARKER = '? ⟶';
|
|
1352
|
+
const BOSS_REPLY_MARKER = 'Boss ⟶';
|
|
1353
|
+
|
|
1354
|
+
function bossQuestionBlock(question: string): string {
|
|
1355
|
+
return `${BOSS_QUESTION_MARKER}\n${question}`;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
function bossReplyBlock(reply: string): string {
|
|
1359
|
+
return `${BOSS_REPLY_MARKER}\n${reply}`;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
/** DR-090 §2: the marked continuation of a call after a Boss reply. */
|
|
1363
|
+
function markedContinuationBlocks(input: {
|
|
1364
|
+
pendingBossQuestion?: { readonly question: string };
|
|
1365
|
+
bossReply?: string;
|
|
1366
|
+
}, resuming = false): string[] {
|
|
1367
|
+
if (input.pendingBossQuestion === undefined || input.bossReply === undefined) {
|
|
1368
|
+
return [];
|
|
1369
|
+
}
|
|
1370
|
+
return [
|
|
1371
|
+
...(resuming ? [] : [bossQuestionBlock(input.pendingBossQuestion.question)]),
|
|
1372
|
+
bossReplyBlock(input.bossReply),
|
|
1373
|
+
];
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
/**
|
|
1377
|
+
* The input a composer receives: without the question and reply, which the
|
|
1378
|
+
* runtime marks itself (DR-090 §2), so a composer adds no labelled blocks.
|
|
1379
|
+
*/
|
|
1380
|
+
function withoutContinuation<T extends {
|
|
1381
|
+
pendingBossQuestion?: unknown;
|
|
1382
|
+
bossReply?: unknown;
|
|
1383
|
+
}>(input: T): T {
|
|
1384
|
+
if (input.pendingBossQuestion === undefined && input.bossReply === undefined) {
|
|
1385
|
+
return input;
|
|
1386
|
+
}
|
|
1387
|
+
const { pendingBossQuestion: _question, bossReply: _reply, ...rest } = input;
|
|
1388
|
+
return rest as T;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
/**
|
|
1392
|
+
* DR-090 §1: the run's Boss exchanges a player call carries before its body,
|
|
1393
|
+
* leaving out the one exchange the call's own continuation blocks carry, so
|
|
1394
|
+
* the asking role reads its answered question once.
|
|
1395
|
+
*/
|
|
1396
|
+
function bossExchangeBlocks(
|
|
1397
|
+
exchanges: readonly PlaybookBossExchange[],
|
|
1398
|
+
input: Pick<PlaybookPlayerInput, 'pendingBossQuestion' | 'bossReply'>,
|
|
1399
|
+
): string[] {
|
|
1400
|
+
let carried = -1;
|
|
1401
|
+
if (input.pendingBossQuestion !== undefined && input.bossReply !== undefined) {
|
|
1402
|
+
const question = input.pendingBossQuestion.question;
|
|
1403
|
+
for (let index = exchanges.length - 1; index >= 0; index -= 1) {
|
|
1404
|
+
const exchange = exchanges[index]!;
|
|
1405
|
+
if (exchange.question === question && exchange.reply === input.bossReply) {
|
|
1406
|
+
carried = index;
|
|
1407
|
+
break;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
return exchanges.flatMap((exchange, index) =>
|
|
1412
|
+
index === carried
|
|
1413
|
+
? []
|
|
1414
|
+
: [bossQuestionBlock(exchange.question), bossReplyBlock(exchange.reply)],
|
|
1415
|
+
);
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1344
1418
|
/** Add clarification context without repeating the question in a live conversation. */
|
|
1345
1419
|
export function composePlayerContinuation(
|
|
1346
1420
|
input: Pick<PlaybookPlayerInput, 'pendingBossQuestion' | 'bossReply'>,
|
|
@@ -1800,19 +1874,25 @@ export function renderGovernedOutcomeContract(
|
|
|
1800
1874
|
for (const [field, authority] of Object.entries(outcome?.fields ?? {})) {
|
|
1801
1875
|
if (authority === 'semantic') {
|
|
1802
1876
|
const entry = authored.get(field);
|
|
1803
|
-
// DR-089: the value has the shape the compiled machine admits
|
|
1877
|
+
// DR-089: the value has the shape the compiled machine admits; a
|
|
1878
|
+
// shape the probe did not reveal is the machine's to judge, so the
|
|
1879
|
+
// example asserts none (DR-091).
|
|
1804
1880
|
const shape =
|
|
1805
1881
|
shapes !== undefined && Object.prototype.hasOwnProperty.call(shapes, field)
|
|
1806
1882
|
? shapes[field]!
|
|
1807
|
-
:
|
|
1808
|
-
const words = describePlaybookSemanticValueShape(shape);
|
|
1883
|
+
: undefined;
|
|
1884
|
+
const words = shape === undefined ? undefined : describePlaybookSemanticValueShape(shape);
|
|
1809
1885
|
replyMembers.push(
|
|
1810
1886
|
`${JSON.stringify(field)}: ${
|
|
1811
1887
|
entry?.placeholder ??
|
|
1812
|
-
(shape ===
|
|
1888
|
+
(shape === undefined
|
|
1889
|
+
? '<value>'
|
|
1890
|
+
: shape === 'string'
|
|
1891
|
+
? '<string>'
|
|
1892
|
+
: `<${words!.replace(/^a /, '')}>`)
|
|
1813
1893
|
}`,
|
|
1814
1894
|
);
|
|
1815
|
-
if (shape !== 'string') valueShapes.push(`\`${field}\` is ${words}`);
|
|
1895
|
+
if (shape !== undefined && shape !== 'string') valueShapes.push(`\`${field}\` is ${words}`);
|
|
1816
1896
|
semanticAsAuthored.push(entry?.segment ?? `\`${field}\``);
|
|
1817
1897
|
} else {
|
|
1818
1898
|
runtimeSupplied.push(`\`${field}\` (${authority}-owned)`);
|
|
@@ -3273,8 +3353,9 @@ function snapshotOutcomeAuthority(
|
|
|
3273
3353
|
// the machine's transition selection — guards only, no action runs — with
|
|
3274
3354
|
// sample values, and the first combination of shapes whose arm carries that
|
|
3275
3355
|
// outcome's accepted-outcome marker is the contract the judge is shown and the
|
|
3276
|
-
// reconciler enforces. A field
|
|
3277
|
-
//
|
|
3356
|
+
// reconciler enforces. A field the machine admits as a string is recorded as
|
|
3357
|
+
// one; a field whose shape it does not reveal is recorded with none, so the
|
|
3358
|
+
// reconciler leaves its value to the machine's own validator (DR-091).
|
|
3278
3359
|
// ---------------------------------------------------------------------------
|
|
3279
3360
|
|
|
3280
3361
|
const SEMANTIC_SHAPE_SAMPLES: readonly (readonly [
|
|
@@ -3337,9 +3418,10 @@ function shapeCombinations(fieldCount: number): readonly number[][] {
|
|
|
3337
3418
|
}
|
|
3338
3419
|
|
|
3339
3420
|
/**
|
|
3340
|
-
* DR-089: the
|
|
3341
|
-
*
|
|
3342
|
-
*
|
|
3421
|
+
* DR-089: the JSON value shape the compiled machine admits for each
|
|
3422
|
+
* semantic-owned field of each governed outcome, keyed by state id and then
|
|
3423
|
+
* by outcome. A field, outcome, or state the probe reveals no shape for is
|
|
3424
|
+
* left out (DR-091). Exported so linked machinery of an artifact's own reads
|
|
3343
3425
|
* the same contract the shared factory enforces.
|
|
3344
3426
|
*/
|
|
3345
3427
|
export function inferSemanticValueShapes(
|
|
@@ -3425,17 +3507,17 @@ export function inferSemanticValueShapes(
|
|
|
3425
3507
|
values[field] = SEMANTIC_SHAPE_SAMPLES[combination[index]!]![1];
|
|
3426
3508
|
});
|
|
3427
3509
|
if (!admits(values)) continue;
|
|
3510
|
+
// DR-091: an admitted string is recorded too, so only a field the
|
|
3511
|
+
// probe revealed nothing for goes unrecorded.
|
|
3428
3512
|
const shapes: Record<string, PlaybookSemanticValueShape> = {};
|
|
3429
3513
|
semanticFields.forEach((field, index) => {
|
|
3430
3514
|
let shape = SEMANTIC_SHAPE_SAMPLES[combination[index]!]![0];
|
|
3431
3515
|
if (shape === 'string-array' && admits({ ...values, [field]: [1] })) {
|
|
3432
3516
|
shape = 'string-or-number-array';
|
|
3433
3517
|
}
|
|
3434
|
-
|
|
3518
|
+
shapes[field] = shape;
|
|
3435
3519
|
});
|
|
3436
|
-
|
|
3437
|
-
stateShapes[guard] = Object.freeze(shapes);
|
|
3438
|
-
}
|
|
3520
|
+
stateShapes[guard] = Object.freeze(shapes);
|
|
3439
3521
|
break;
|
|
3440
3522
|
}
|
|
3441
3523
|
}
|
|
@@ -4269,10 +4351,16 @@ export function createXStatePlaybookRuntime<
|
|
|
4269
4351
|
spec.composePlayerPrompt ??
|
|
4270
4352
|
((input: PlaybookPlayerInput, _identity: XStatePromptIdentity, resuming = false) =>
|
|
4271
4353
|
defaultComposePlayerPrompt(input, spec.placeholderFields, resuming));
|
|
4272
|
-
const
|
|
4354
|
+
const composeArtifactCaptainPrompt =
|
|
4273
4355
|
spec.composeCaptainPrompt ??
|
|
4274
4356
|
((input: PlaybookCaptainInput) =>
|
|
4275
4357
|
defaultComposeCaptainPrompt(input, spec.placeholderFields));
|
|
4358
|
+
// DR-090 §2: a direct-Captain continuation is marked by the runtime too.
|
|
4359
|
+
const composeCaptainPrompt = (input: PlaybookCaptainInput): string =>
|
|
4360
|
+
[
|
|
4361
|
+
...markedContinuationBlocks(input),
|
|
4362
|
+
composeArtifactCaptainPrompt(withoutContinuation(input)),
|
|
4363
|
+
].join('\n\n');
|
|
4276
4364
|
const extractFields =
|
|
4277
4365
|
spec.extractRequiredFields ?? defaultExtractRequiredFields;
|
|
4278
4366
|
const verbatimPayloadFields: ReadonlySet<string> = new Set(
|
|
@@ -4508,6 +4596,9 @@ export function createXStatePlaybookRuntime<
|
|
|
4508
4596
|
);
|
|
4509
4597
|
let effectLedgerMirror = currentEffectLedger();
|
|
4510
4598
|
let recoveryCheckpoint: PlaybookRuntimeSnapshot['recoveryCheckpoint'];
|
|
4599
|
+
// DR-090 §1: the run's Boss exchanges, in order. A run begins at the
|
|
4600
|
+
// artifact's entry event; the list travels in the snapshot.
|
|
4601
|
+
let bossExchanges: readonly PlaybookBossExchange[] = [];
|
|
4511
4602
|
let retainedEffectSourceSessionId: string | undefined;
|
|
4512
4603
|
let retainedEffectReconciliation:
|
|
4513
4604
|
| PlaybookRuntimeSnapshot['retainedEffectReconciliation']
|
|
@@ -5406,7 +5497,18 @@ export function createXStatePlaybookRuntime<
|
|
|
5406
5497
|
return session?.roleBindings?.[roleId]?.promptIdentity ?? roleId;
|
|
5407
5498
|
}
|
|
5408
5499
|
|
|
5500
|
+
// DR-090 §3: the first block names the role and, where the host bound
|
|
5501
|
+
// it, the model the call runs on as configured, so a player can name
|
|
5502
|
+
// both where its project asks (a commit trailer, say).
|
|
5503
|
+
function playerIdentityBlock(roleId: string): string {
|
|
5504
|
+
const binding = session?.roleBindings?.[roleId];
|
|
5505
|
+
return binding === undefined
|
|
5506
|
+
? `Role: ${roleId}`
|
|
5507
|
+
: `Role: ${roleId} · Model: ${binding.promptIdentity}`;
|
|
5508
|
+
}
|
|
5509
|
+
|
|
5409
5510
|
function composeBoundPlayerPrompt(input: PlaybookPlayerInput, resuming = false): string {
|
|
5511
|
+
const roleId = requireRoleId(input);
|
|
5410
5512
|
let active = true;
|
|
5411
5513
|
const lookup: XStatePromptIdentity = (roleId) => {
|
|
5412
5514
|
if (!active) {
|
|
@@ -5416,11 +5518,49 @@ export function createXStatePlaybookRuntime<
|
|
|
5416
5518
|
}
|
|
5417
5519
|
return promptIdentity(roleId);
|
|
5418
5520
|
};
|
|
5521
|
+
let body: string;
|
|
5419
5522
|
try {
|
|
5420
|
-
|
|
5523
|
+
body = composePlayerPrompt(withoutContinuation(input), lookup, resuming);
|
|
5421
5524
|
} finally {
|
|
5422
5525
|
active = false;
|
|
5423
5526
|
}
|
|
5527
|
+
// DR-090 §1, §2: every role hears the Boss — the run's exchanges come
|
|
5528
|
+
// before the body whatever the role, and the asking role's marked
|
|
5529
|
+
// continuation carries the exchange it asked.
|
|
5530
|
+
return [
|
|
5531
|
+
playerIdentityBlock(roleId),
|
|
5532
|
+
...bossExchangeBlocks(bossExchanges, input),
|
|
5533
|
+
...markedContinuationBlocks(input, resuming),
|
|
5534
|
+
body,
|
|
5535
|
+
].join('\n\n');
|
|
5536
|
+
}
|
|
5537
|
+
|
|
5538
|
+
/** DR-090 §1: record a Boss reply the machine is about to receive. */
|
|
5539
|
+
function recordBossExchange(
|
|
5540
|
+
event: EventObject,
|
|
5541
|
+
pending: readonly PlaybookPendingBossQuestionContext[],
|
|
5542
|
+
): void {
|
|
5543
|
+
if (event.type !== 'BOSS_REPLY') return;
|
|
5544
|
+
const { questionId, answer } = event as {
|
|
5545
|
+
readonly questionId?: unknown;
|
|
5546
|
+
readonly answer?: unknown;
|
|
5547
|
+
};
|
|
5548
|
+
if (typeof answer !== 'string' || answer.trim().length === 0) return;
|
|
5549
|
+
const question =
|
|
5550
|
+
pending.find((candidate) => candidate.questionId === questionId) ??
|
|
5551
|
+
(questionId === undefined && pending.length === 1 ? pending[0] : undefined);
|
|
5552
|
+
if (question === undefined) return;
|
|
5553
|
+
bossExchanges = Object.freeze([
|
|
5554
|
+
...bossExchanges,
|
|
5555
|
+
Object.freeze({ question: question.question, reply: answer }),
|
|
5556
|
+
]);
|
|
5557
|
+
}
|
|
5558
|
+
|
|
5559
|
+
/** DR-090 §1: an entry event begins a new run, with no exchanges yet. */
|
|
5560
|
+
function beginRunIfEntry(event: EventObject): void {
|
|
5561
|
+
if (spec.entryEvent !== undefined && event.type === spec.entryEvent.type) {
|
|
5562
|
+
bossExchanges = [];
|
|
5563
|
+
}
|
|
5424
5564
|
}
|
|
5425
5565
|
|
|
5426
5566
|
function continuationKey(
|
|
@@ -6706,7 +6846,10 @@ export function createXStatePlaybookRuntime<
|
|
|
6706
6846
|
try {
|
|
6707
6847
|
await claim.assertOwner();
|
|
6708
6848
|
const restored = snapshotJsonValue(await repositoryCapability!.observe!()) as unknown as PlaybookRepositoryReceipt['baseline'];
|
|
6709
|
-
|
|
6849
|
+
// DR-090 §4: the branch an observation names is never compared.
|
|
6850
|
+
const { branch: _restoredBranch, ...restoredState } = restored;
|
|
6851
|
+
const { branch: _savedBranch, ...savedState } = saved.baseline;
|
|
6852
|
+
if (!isDeepStrictEqual(restoredState, savedState)) {
|
|
6710
6853
|
throw new Error(`${label} preparation has not restored the stopped step's repository; it remains paused`);
|
|
6711
6854
|
}
|
|
6712
6855
|
signal.throwIfAborted();
|
|
@@ -6954,6 +7097,14 @@ export function createXStatePlaybookRuntime<
|
|
|
6954
7097
|
...((state.stateId === 'failed' || pendingQuestions.length > 0) && recoveryCheckpoint !== undefined
|
|
6955
7098
|
? { recoveryCheckpoint } : {}),
|
|
6956
7099
|
...(suspendedCall === undefined ? {} : { suspendedCall }),
|
|
7100
|
+
...(bossExchanges.length === 0
|
|
7101
|
+
? {}
|
|
7102
|
+
: {
|
|
7103
|
+
bossExchanges: bossExchanges.map(({ question, reply }) => ({
|
|
7104
|
+
question,
|
|
7105
|
+
reply,
|
|
7106
|
+
})),
|
|
7107
|
+
}),
|
|
6957
7108
|
};
|
|
6958
7109
|
}
|
|
6959
7110
|
|
|
@@ -9224,6 +9375,7 @@ export function createXStatePlaybookRuntime<
|
|
|
9224
9375
|
deferInspectionEmissions = false;
|
|
9225
9376
|
deferredInspectionEmissions = [];
|
|
9226
9377
|
recoveryCheckpoint = undefined;
|
|
9378
|
+
bossExchanges = [];
|
|
9227
9379
|
suppressInspectionEmissions = false;
|
|
9228
9380
|
initialized = false;
|
|
9229
9381
|
traceSequence = 0;
|
|
@@ -9671,6 +9823,13 @@ export function createXStatePlaybookRuntime<
|
|
|
9671
9823
|
}
|
|
9672
9824
|
restoreRoleResumeTokens(boundSnapshot.roleResumeTokens);
|
|
9673
9825
|
}
|
|
9826
|
+
// DR-090 §1: a restored or adopted run keeps the Boss exchanges it
|
|
9827
|
+
// heard; a snapshot without them heard none.
|
|
9828
|
+
bossExchanges = Object.freeze(
|
|
9829
|
+
(boundSnapshot.bossExchanges ?? []).map(({ question, reply }) =>
|
|
9830
|
+
Object.freeze({ question, reply }),
|
|
9831
|
+
),
|
|
9832
|
+
);
|
|
9674
9833
|
nestedBridge.prepareRestore(suspendedCall);
|
|
9675
9834
|
if (suspendedCall !== undefined) {
|
|
9676
9835
|
playbookCallTurnIds.set(
|
|
@@ -10870,13 +11029,20 @@ export function createXStatePlaybookRuntime<
|
|
|
10870
11029
|
)
|
|
10871
11030
|
) {
|
|
10872
11031
|
const statusLine = classificationStatus(event);
|
|
11032
|
+
const acceptedPending = deferredPending;
|
|
11033
|
+
const acceptedEvent = event;
|
|
10873
11034
|
const continuation = await continueBoundDeferredOperation(
|
|
10874
11035
|
deferredOperation,
|
|
10875
11036
|
event,
|
|
10876
11037
|
signal,
|
|
10877
11038
|
turnId,
|
|
10878
11039
|
statusLine,
|
|
10879
|
-
|
|
11040
|
+
() => {
|
|
11041
|
+
// DR-090 §1: the host started the continuation, so the
|
|
11042
|
+
// machine receives this reply now.
|
|
11043
|
+
recordBossExchange(acceptedEvent, [acceptedPending]);
|
|
11044
|
+
onAccepted?.();
|
|
11045
|
+
},
|
|
10880
11046
|
);
|
|
10881
11047
|
if (continuation === 'continued') {
|
|
10882
11048
|
if (controlPlaneError !== undefined) {
|
|
@@ -10921,13 +11087,25 @@ export function createXStatePlaybookRuntime<
|
|
|
10921
11087
|
signal.throwIfAborted();
|
|
10922
11088
|
// 3. A final actor cannot accept new events; reconstruct only
|
|
10923
11089
|
// after classification produced a real event.
|
|
11090
|
+
const acceptedPending =
|
|
11091
|
+
classifiedSnapshot === undefined
|
|
11092
|
+
? []
|
|
11093
|
+
: pendingBossQuestionsFor(
|
|
11094
|
+
normalizePlaybookSnapshot(classifiedSnapshot),
|
|
11095
|
+
((classifiedSnapshot as { context?: unknown }).context ??
|
|
11096
|
+
{}) as Record<string, unknown>,
|
|
11097
|
+
);
|
|
10924
11098
|
if (actor.getSnapshot().status === 'done') {
|
|
10925
11099
|
stopActor();
|
|
10926
11100
|
actor = buildActor(runtimePorts!);
|
|
10927
11101
|
// The replacement actor's snapshots are real state entries.
|
|
10928
11102
|
suppressInspectionEmissions = false;
|
|
10929
11103
|
actor.start();
|
|
11104
|
+
// DR-090 §1: a finished run's exchanges end with it.
|
|
11105
|
+
bossExchanges = [];
|
|
10930
11106
|
}
|
|
11107
|
+
beginRunIfEntry(event);
|
|
11108
|
+
recordBossExchange(event, acceptedPending);
|
|
10931
11109
|
onAccepted?.();
|
|
10932
11110
|
actor.send(event);
|
|
10933
11111
|
await waitForPlaybookQuiescence(actor, {
|
|
@@ -11236,6 +11414,7 @@ export function createXStatePlaybookRuntime<
|
|
|
11236
11414
|
failedGovernedAttemptId = undefined;
|
|
11237
11415
|
controlPlaneError = undefined;
|
|
11238
11416
|
emissionFailure = undefined;
|
|
11417
|
+
bossExchanges = [];
|
|
11239
11418
|
retainedEffectSourceSessionId = undefined;
|
|
11240
11419
|
retainedEffectReconciliation = undefined;
|
|
11241
11420
|
retainedEffectReconciliationRequired = false;
|
package/src/xstate-runtime.d.ts
CHANGED
|
@@ -56,13 +56,14 @@ export interface PlaybookSemanticOutcomeSpec {
|
|
|
56
56
|
}
|
|
57
57
|
/**
|
|
58
58
|
* The JSON value shape a compiled machine admits for one semantic-owned field
|
|
59
|
-
* (DR-089). A field the machine admits as a string
|
|
60
|
-
* machine does not reveal
|
|
59
|
+
* (DR-089). A field the machine admits as a string is `string`; a field whose
|
|
60
|
+
* shape the machine does not reveal has none (DR-091).
|
|
61
61
|
*/
|
|
62
62
|
export type PlaybookSemanticValueShape = 'string' | 'string-array' | 'number-array' | 'string-or-number-array' | 'number' | 'boolean';
|
|
63
63
|
/**
|
|
64
|
-
* Per outcome, the
|
|
65
|
-
*
|
|
64
|
+
* Per outcome, the shapes of its semantic-owned fields; a field left out has
|
|
65
|
+
* no revealed shape and takes any JSON value, the machine's own validator
|
|
66
|
+
* deciding it (DR-091).
|
|
66
67
|
*/
|
|
67
68
|
export type PlaybookSemanticValueShapes = Readonly<Record<string, Readonly<Record<string, PlaybookSemanticValueShape>>>>;
|
|
68
69
|
/** The words a judge prompt or validation error uses for one value shape. */
|
|
@@ -76,7 +77,8 @@ export interface PlaybookSemanticEvidenceInput {
|
|
|
76
77
|
readonly runtimeFields?: unknown;
|
|
77
78
|
/**
|
|
78
79
|
* DR-089: the shape the compiled machine admits for each semantic-owned
|
|
79
|
-
* field, keyed by outcome;
|
|
80
|
+
* field, keyed by outcome; a field it leaves out takes any JSON value
|
|
81
|
+
* (DR-091).
|
|
80
82
|
*/
|
|
81
83
|
readonly semanticValueShapes?: PlaybookSemanticValueShapes;
|
|
82
84
|
}
|
package/src/xstate-runtime.js
CHANGED
|
@@ -681,6 +681,13 @@ function effectInteger(value, path, minimum = 0) {
|
|
|
681
681
|
}
|
|
682
682
|
return value;
|
|
683
683
|
}
|
|
684
|
+
// DR-090 §4: an observation's branch is evidence for the Boss only; two
|
|
685
|
+
// observations of one repository state are equal whatever branch each names.
|
|
686
|
+
function observedStatesEqual(left, right) {
|
|
687
|
+
const { branch: _left, ...leftState } = left;
|
|
688
|
+
const { branch: _right, ...rightState } = right;
|
|
689
|
+
return jsonValuesEqual(leftState, rightState);
|
|
690
|
+
}
|
|
684
691
|
function jsonValuesEqual(left, right) {
|
|
685
692
|
if (Object.is(left, right))
|
|
686
693
|
return true;
|
|
@@ -760,13 +767,17 @@ function effectPreExisting(value, path, baseline) {
|
|
|
760
767
|
function effectObservation(value, path) {
|
|
761
768
|
if (!isRecord(value))
|
|
762
769
|
throw new TypeError(`${path} must be an object`);
|
|
763
|
-
rejectUnknownKeys(value, ['worktree', 'gitDir', 'head', 'projection', 'projectionDigest'], path);
|
|
770
|
+
rejectUnknownKeys(value, ['worktree', 'gitDir', 'head', 'branch', 'projection', 'projectionDigest'], path);
|
|
764
771
|
requireNonEmptyString(value.worktree, `${path}.worktree`);
|
|
765
772
|
requireNonEmptyString(value.gitDir, `${path}.gitDir`);
|
|
766
773
|
const head = requireNonEmptyString(value.head, `${path}.head`);
|
|
767
774
|
if (!EFFECT_OID_PATTERN.test(head)) {
|
|
768
775
|
throw new TypeError(`${path}.head must be a canonical Git commit OID`);
|
|
769
776
|
}
|
|
777
|
+
// DR-090 §4: the branch HEAD names, evidence for the Boss only.
|
|
778
|
+
if (own(value, 'branch')) {
|
|
779
|
+
requireNonEmptyString(value.branch, `${path}.branch`);
|
|
780
|
+
}
|
|
770
781
|
if (!isRecord(value.projection)) {
|
|
771
782
|
throw new TypeError(`${path}.projection must be a path-keyed object`);
|
|
772
783
|
}
|
|
@@ -854,7 +865,7 @@ function effectReceipt(value, path, expectedBaseline, expectedAfter, matchExpect
|
|
|
854
865
|
}
|
|
855
866
|
if (classification === 'unchanged' &&
|
|
856
867
|
after !== undefined &&
|
|
857
|
-
!
|
|
868
|
+
!observedStatesEqual(baseline, after)) {
|
|
858
869
|
throw new TypeError(`${path} classified unchanged observations that differ`);
|
|
859
870
|
}
|
|
860
871
|
// DR-062 §3: a receipt records the fate of every baseline entry, so the
|
|
@@ -997,12 +1008,13 @@ function snapshotSemanticCandidate(value, outcomes, shapes) {
|
|
|
997
1008
|
(extra.length === 0 ? '' : `; extra or wrongly owned ${extra.join(', ')}`));
|
|
998
1009
|
}
|
|
999
1010
|
// DR-089: each semantic-owned field has the shape the compiled machine
|
|
1000
|
-
// admits for it
|
|
1011
|
+
// admits for it. DR-091: a field whose shape the machine did not reveal
|
|
1012
|
+
// takes any JSON value, and the machine's own validator decides it.
|
|
1001
1013
|
const outcomeShapes = shapes !== undefined && own(shapes, guard) ? shapes[guard] : undefined;
|
|
1002
1014
|
for (const field of semanticFields) {
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1015
|
+
if (outcomeShapes === undefined || !own(outcomeShapes, field))
|
|
1016
|
+
continue;
|
|
1017
|
+
const shape = outcomeShapes[field];
|
|
1006
1018
|
if (!isPlaybookSemanticValueShape(detached[field], shape)) {
|
|
1007
1019
|
semanticCandidateStructureError(shape === 'string'
|
|
1008
1020
|
? `field ${JSON.stringify(field)} must be a string`
|
|
@@ -1417,7 +1429,7 @@ function effectBoundary(value, index) {
|
|
|
1417
1429
|
}
|
|
1418
1430
|
if (own(value, 'restored')) {
|
|
1419
1431
|
const restored = effectObservation(value.restored, `${path}.restored`);
|
|
1420
|
-
if (!
|
|
1432
|
+
if (!observedStatesEqual(restored, baseline) ||
|
|
1421
1433
|
!isRecord(value.physicalReceipt) || after?.head !== baseline.head ||
|
|
1422
1434
|
value.dispositions.some((item) => item !== 'unchanged') ||
|
|
1423
1435
|
value.cohortId !== undefined || value.logicalOperationId !== undefined) {
|
|
@@ -1506,7 +1518,7 @@ function effectLogicalOperation(value, index, boundaryById) {
|
|
|
1506
1518
|
const previous = boundaries[boundaryIndex - 1];
|
|
1507
1519
|
if (previous.physicalReceipt === undefined ||
|
|
1508
1520
|
previous.after === undefined ||
|
|
1509
|
-
!
|
|
1521
|
+
!observedStatesEqual(boundary.baseline, previous.after)) {
|
|
1510
1522
|
throw new TypeError(`${path}.boundaryIds must form one completed checkpoint chain`);
|
|
1511
1523
|
}
|
|
1512
1524
|
}
|
|
@@ -1517,7 +1529,7 @@ function effectLogicalOperation(value, index, boundaryById) {
|
|
|
1517
1529
|
if (checkpoint !== undefined) {
|
|
1518
1530
|
assertObservationIdentity(checkpoint, originalBaseline, `${path}.checkpoint`);
|
|
1519
1531
|
if (latestAfter === undefined ||
|
|
1520
|
-
!
|
|
1532
|
+
!observedStatesEqual(checkpoint, latestAfter)) {
|
|
1521
1533
|
throw new TypeError(`${path}.checkpoint must equal its latest boundary after`);
|
|
1522
1534
|
}
|
|
1523
1535
|
}
|
|
@@ -1810,6 +1822,7 @@ export function assertPlaybookRuntimeSnapshot(value, expectedPlaybookId, options
|
|
|
1810
1822
|
'failedEffectAttempt',
|
|
1811
1823
|
'recoveryCheckpoint',
|
|
1812
1824
|
'suspendedCall',
|
|
1825
|
+
'bossExchanges',
|
|
1813
1826
|
], 'runtime snapshot');
|
|
1814
1827
|
let suspendedCall;
|
|
1815
1828
|
if (own(snapshot, 'suspendedCall')) {
|
|
@@ -1994,6 +2007,23 @@ export function assertPlaybookRuntimeSnapshot(value, expectedPlaybookId, options
|
|
|
1994
2007
|
}
|
|
1995
2008
|
recoveryCheckpoint = Object.freeze({ stateId, prompt, boundaryPrefix, machine: snapshotJsonValue(checkpoint.machine), ...(own(checkpoint, 'id') ? { id: effectUuid(checkpoint.id, 'recoveryCheckpoint.id') } : {}), ...(own(checkpoint, 'result') ? { result: snapshotJsonValue(checkpoint.result) } : {}), ...(checkpoint.delivered === true ? { delivered: true } : {}) });
|
|
1996
2009
|
}
|
|
2010
|
+
// DR-090 §1: the run's Boss exchanges, present only when there is one.
|
|
2011
|
+
let bossExchanges;
|
|
2012
|
+
if (own(snapshot, 'bossExchanges')) {
|
|
2013
|
+
const list = snapshot.bossExchanges;
|
|
2014
|
+
if (!Array.isArray(list) || list.length === 0) {
|
|
2015
|
+
throw new TypeError('runtime snapshot bossExchanges must be a nonempty list');
|
|
2016
|
+
}
|
|
2017
|
+
bossExchanges = Object.freeze(list.map((exchange, index) => {
|
|
2018
|
+
const path = `runtime snapshot bossExchanges[${index}]`;
|
|
2019
|
+
if (!isRecord(exchange))
|
|
2020
|
+
throw new TypeError(`${path} must be an object`);
|
|
2021
|
+
rejectUnknownKeys(exchange, ['question', 'reply'], path);
|
|
2022
|
+
const question = requireNonEmptyString(exchange.question, `${path}.question`);
|
|
2023
|
+
const reply = requireNonEmptyString(exchange.reply, `${path}.reply`);
|
|
2024
|
+
return Object.freeze({ question, reply });
|
|
2025
|
+
}));
|
|
2026
|
+
}
|
|
1997
2027
|
const fields = {
|
|
1998
2028
|
playbookId,
|
|
1999
2029
|
machine,
|
|
@@ -2017,6 +2047,7 @@ export function assertPlaybookRuntimeSnapshot(value, expectedPlaybookId, options
|
|
|
2017
2047
|
schemaVersion: 4,
|
|
2018
2048
|
...fields,
|
|
2019
2049
|
...(suspendedCall === undefined ? {} : { suspendedCall }),
|
|
2050
|
+
...(bossExchanges === undefined ? {} : { bossExchanges }),
|
|
2020
2051
|
});
|
|
2021
2052
|
}
|
|
2022
2053
|
export class NestedPlaybookCallError extends Error {
|