@sublang/playbook 17.8.0 → 17.9.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/package.json +1 -1
- package/reference/sdlc/captain.md +3 -3
- package/reference/sdlc/captain.playbook/captain.fsm.js +3 -3
- package/reference/sdlc/captain.playbook/captain.fsm.ts +3 -3
- package/reference/sdlc/captain.playbook/captain.gears.md +3 -3
- package/reference/sdlc/code.playbook/bin/repository-effects.js +26 -2
- package/reference/sdlc/code.playbook/host-capabilities.d.ts +1 -0
- package/reference/sdlc/code.playbook/playbook-captain.js +157 -46
- package/reference/sdlc/code.playbook/playbook-captain.ts +210 -59
- 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 +152 -14
- package/src/xstate-playbook-runtime.ts +186 -13
- package/src/xstate-runtime.d.ts +7 -5
- package/src/xstate-runtime.js +40 -9
- package/src/xstate-runtime.ts +53 -24
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 {
|
package/src/xstate-runtime.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
|
|
14
14
|
import { assertPlaybookFailureCause } from './runtime.js';
|
|
15
15
|
import type {
|
|
16
|
+
PlaybookBossExchange,
|
|
16
17
|
CaptainResult,
|
|
17
18
|
JsonValue,
|
|
18
19
|
NormalizedError,
|
|
@@ -992,6 +993,20 @@ function effectInteger(value: unknown, path: string, minimum = 0): number {
|
|
|
992
993
|
return value as number;
|
|
993
994
|
}
|
|
994
995
|
|
|
996
|
+
// DR-090 §4: an observation's branch is evidence for the Boss only; two
|
|
997
|
+
// observations of one repository state are equal whatever branch each names.
|
|
998
|
+
function observedStatesEqual(
|
|
999
|
+
left: PlaybookRepositoryObservation,
|
|
1000
|
+
right: PlaybookRepositoryObservation,
|
|
1001
|
+
): boolean {
|
|
1002
|
+
const { branch: _left, ...leftState } = left;
|
|
1003
|
+
const { branch: _right, ...rightState } = right;
|
|
1004
|
+
return jsonValuesEqual(
|
|
1005
|
+
leftState as unknown as JsonValue,
|
|
1006
|
+
rightState as unknown as JsonValue,
|
|
1007
|
+
);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
995
1010
|
function jsonValuesEqual(left: JsonValue, right: JsonValue): boolean {
|
|
996
1011
|
if (Object.is(left, right)) return true;
|
|
997
1012
|
if (Array.isArray(left) || Array.isArray(right)) {
|
|
@@ -1110,7 +1125,7 @@ function effectObservation(
|
|
|
1110
1125
|
if (!isRecord(value)) throw new TypeError(`${path} must be an object`);
|
|
1111
1126
|
rejectUnknownKeys(
|
|
1112
1127
|
value,
|
|
1113
|
-
['worktree', 'gitDir', 'head', 'projection', 'projectionDigest'],
|
|
1128
|
+
['worktree', 'gitDir', 'head', 'branch', 'projection', 'projectionDigest'],
|
|
1114
1129
|
path,
|
|
1115
1130
|
);
|
|
1116
1131
|
requireNonEmptyString(value.worktree, `${path}.worktree`);
|
|
@@ -1119,6 +1134,10 @@ function effectObservation(
|
|
|
1119
1134
|
if (!EFFECT_OID_PATTERN.test(head)) {
|
|
1120
1135
|
throw new TypeError(`${path}.head must be a canonical Git commit OID`);
|
|
1121
1136
|
}
|
|
1137
|
+
// DR-090 §4: the branch HEAD names, evidence for the Boss only.
|
|
1138
|
+
if (own(value, 'branch')) {
|
|
1139
|
+
requireNonEmptyString(value.branch, `${path}.branch`);
|
|
1140
|
+
}
|
|
1122
1141
|
if (!isRecord(value.projection)) {
|
|
1123
1142
|
throw new TypeError(`${path}.projection must be a path-keyed object`);
|
|
1124
1143
|
}
|
|
@@ -1252,10 +1271,7 @@ function effectReceipt(
|
|
|
1252
1271
|
if (
|
|
1253
1272
|
classification === 'unchanged' &&
|
|
1254
1273
|
after !== undefined &&
|
|
1255
|
-
!
|
|
1256
|
-
baseline as unknown as JsonValue,
|
|
1257
|
-
after as unknown as JsonValue,
|
|
1258
|
-
)
|
|
1274
|
+
!observedStatesEqual(baseline, after)
|
|
1259
1275
|
) {
|
|
1260
1276
|
throw new TypeError(`${path} classified unchanged observations that differ`);
|
|
1261
1277
|
}
|
|
@@ -1370,8 +1386,8 @@ export interface PlaybookSemanticOutcomeSpec {
|
|
|
1370
1386
|
|
|
1371
1387
|
/**
|
|
1372
1388
|
* The JSON value shape a compiled machine admits for one semantic-owned field
|
|
1373
|
-
* (DR-089). A field the machine admits as a string
|
|
1374
|
-
* machine does not reveal
|
|
1389
|
+
* (DR-089). A field the machine admits as a string is `string`; a field whose
|
|
1390
|
+
* shape the machine does not reveal has none (DR-091).
|
|
1375
1391
|
*/
|
|
1376
1392
|
export type PlaybookSemanticValueShape =
|
|
1377
1393
|
| 'string'
|
|
@@ -1382,8 +1398,9 @@ export type PlaybookSemanticValueShape =
|
|
|
1382
1398
|
| 'boolean';
|
|
1383
1399
|
|
|
1384
1400
|
/**
|
|
1385
|
-
* Per outcome, the
|
|
1386
|
-
*
|
|
1401
|
+
* Per outcome, the shapes of its semantic-owned fields; a field left out has
|
|
1402
|
+
* no revealed shape and takes any JSON value, the machine's own validator
|
|
1403
|
+
* deciding it (DR-091).
|
|
1387
1404
|
*/
|
|
1388
1405
|
export type PlaybookSemanticValueShapes = Readonly<
|
|
1389
1406
|
Record<string, Readonly<Record<string, PlaybookSemanticValueShape>>>
|
|
@@ -1444,7 +1461,8 @@ export interface PlaybookSemanticEvidenceInput {
|
|
|
1444
1461
|
readonly runtimeFields?: unknown;
|
|
1445
1462
|
/**
|
|
1446
1463
|
* DR-089: the shape the compiled machine admits for each semantic-owned
|
|
1447
|
-
* field, keyed by outcome;
|
|
1464
|
+
* field, keyed by outcome; a field it leaves out takes any JSON value
|
|
1465
|
+
* (DR-091).
|
|
1448
1466
|
*/
|
|
1449
1467
|
readonly semanticValueShapes?: PlaybookSemanticValueShapes;
|
|
1450
1468
|
}
|
|
@@ -1548,14 +1566,13 @@ function snapshotSemanticCandidate(
|
|
|
1548
1566
|
);
|
|
1549
1567
|
}
|
|
1550
1568
|
// DR-089: each semantic-owned field has the shape the compiled machine
|
|
1551
|
-
// admits for it
|
|
1569
|
+
// admits for it. DR-091: a field whose shape the machine did not reveal
|
|
1570
|
+
// takes any JSON value, and the machine's own validator decides it.
|
|
1552
1571
|
const outcomeShapes =
|
|
1553
1572
|
shapes !== undefined && own(shapes, guard) ? shapes[guard] : undefined;
|
|
1554
1573
|
for (const field of semanticFields) {
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
? outcomeShapes[field]!
|
|
1558
|
-
: 'string';
|
|
1574
|
+
if (outcomeShapes === undefined || !own(outcomeShapes, field)) continue;
|
|
1575
|
+
const shape = outcomeShapes[field]!;
|
|
1559
1576
|
if (!isPlaybookSemanticValueShape(detached[field], shape)) {
|
|
1560
1577
|
semanticCandidateStructureError(
|
|
1561
1578
|
shape === 'string'
|
|
@@ -2099,7 +2116,7 @@ function effectBoundary(value: unknown, index: number): PlaybookEffectBoundary {
|
|
|
2099
2116
|
}
|
|
2100
2117
|
if (own(value, 'restored')) {
|
|
2101
2118
|
const restored = effectObservation(value.restored, `${path}.restored`);
|
|
2102
|
-
if (!
|
|
2119
|
+
if (!observedStatesEqual(restored, baseline) ||
|
|
2103
2120
|
!isRecord(value.physicalReceipt) || after?.head !== baseline.head ||
|
|
2104
2121
|
(value.dispositions as string[]).some((item) => item !== 'unchanged') ||
|
|
2105
2122
|
value.cohortId !== undefined || value.logicalOperationId !== undefined) {
|
|
@@ -2237,10 +2254,7 @@ function effectLogicalOperation(
|
|
|
2237
2254
|
if (
|
|
2238
2255
|
previous.physicalReceipt === undefined ||
|
|
2239
2256
|
previous.after === undefined ||
|
|
2240
|
-
!
|
|
2241
|
-
boundary.baseline as unknown as JsonValue,
|
|
2242
|
-
previous.after as unknown as JsonValue,
|
|
2243
|
-
)
|
|
2257
|
+
!observedStatesEqual(boundary.baseline, previous.after)
|
|
2244
2258
|
) {
|
|
2245
2259
|
throw new TypeError(
|
|
2246
2260
|
`${path}.boundaryIds must form one completed checkpoint chain`,
|
|
@@ -2259,10 +2273,7 @@ function effectLogicalOperation(
|
|
|
2259
2273
|
);
|
|
2260
2274
|
if (
|
|
2261
2275
|
latestAfter === undefined ||
|
|
2262
|
-
!
|
|
2263
|
-
checkpoint as unknown as JsonValue,
|
|
2264
|
-
latestAfter as unknown as JsonValue,
|
|
2265
|
-
)
|
|
2276
|
+
!observedStatesEqual(checkpoint, latestAfter)
|
|
2266
2277
|
) {
|
|
2267
2278
|
throw new TypeError(`${path}.checkpoint must equal its latest boundary after`);
|
|
2268
2279
|
}
|
|
@@ -2737,6 +2748,7 @@ export function assertPlaybookRuntimeSnapshot(
|
|
|
2737
2748
|
'failedEffectAttempt',
|
|
2738
2749
|
'recoveryCheckpoint',
|
|
2739
2750
|
'suspendedCall',
|
|
2751
|
+
'bossExchanges',
|
|
2740
2752
|
],
|
|
2741
2753
|
'runtime snapshot',
|
|
2742
2754
|
);
|
|
@@ -3044,6 +3056,22 @@ export function assertPlaybookRuntimeSnapshot(
|
|
|
3044
3056
|
}
|
|
3045
3057
|
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 as const } : {})});
|
|
3046
3058
|
}
|
|
3059
|
+
// DR-090 §1: the run's Boss exchanges, present only when there is one.
|
|
3060
|
+
let bossExchanges: readonly PlaybookBossExchange[] | undefined;
|
|
3061
|
+
if (own(snapshot, 'bossExchanges')) {
|
|
3062
|
+
const list = snapshot.bossExchanges;
|
|
3063
|
+
if (!Array.isArray(list) || list.length === 0) {
|
|
3064
|
+
throw new TypeError('runtime snapshot bossExchanges must be a nonempty list');
|
|
3065
|
+
}
|
|
3066
|
+
bossExchanges = Object.freeze(list.map((exchange: unknown, index: number) => {
|
|
3067
|
+
const path = `runtime snapshot bossExchanges[${index}]`;
|
|
3068
|
+
if (!isRecord(exchange)) throw new TypeError(`${path} must be an object`);
|
|
3069
|
+
rejectUnknownKeys(exchange, ['question', 'reply'], path);
|
|
3070
|
+
const question = requireNonEmptyString(exchange.question, `${path}.question`);
|
|
3071
|
+
const reply = requireNonEmptyString(exchange.reply, `${path}.reply`);
|
|
3072
|
+
return Object.freeze({ question, reply });
|
|
3073
|
+
}));
|
|
3074
|
+
}
|
|
3047
3075
|
const fields = {
|
|
3048
3076
|
playbookId,
|
|
3049
3077
|
machine,
|
|
@@ -3067,6 +3095,7 @@ export function assertPlaybookRuntimeSnapshot(
|
|
|
3067
3095
|
schemaVersion: 4,
|
|
3068
3096
|
...fields,
|
|
3069
3097
|
...(suspendedCall === undefined ? {} : { suspendedCall }),
|
|
3098
|
+
...(bossExchanges === undefined ? {} : { bossExchanges }),
|
|
3070
3099
|
});
|
|
3071
3100
|
}
|
|
3072
3101
|
|