@sublang/playbook 12.2.2 → 12.3.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/code.playbook/bin/launch-config.js +11 -7
- package/reference/sdlc/code.playbook/bin/session-store.js +16 -5
- package/reference/sdlc/code.playbook/code.fsm.js +43 -5
- package/reference/sdlc/code.playbook/code.fsm.ts +66 -9
- package/reference/sdlc/code.playbook/playbook-captain.js +22 -5
- package/reference/sdlc/code.playbook/playbook-captain.ts +25 -5
- package/reference/sdlc/decide.playbook/decide.fsm.js +4 -0
- package/reference/sdlc/decide.playbook/decide.fsm.ts +4 -0
- package/reference/sdlc/decide.playbook/decide.playbook.js +23 -5
- package/reference/sdlc/decide.playbook/decide.playbook.ts +25 -4
- package/reference/sdlc/dev.playbook/dev.fsm.js +57 -7
- package/reference/sdlc/dev.playbook/dev.fsm.ts +80 -11
- package/reference/sdlc/review.playbook/review.fsm.js +11 -1
- package/reference/sdlc/review.playbook/review.fsm.ts +15 -1
- package/slc/gears2fsm.md +21 -3
- package/slc/link.md +21 -2
- package/src/runtime.d.ts +7 -0
- package/src/runtime.ts +12 -0
- package/src/xstate-playbook-runtime.d.ts +13 -0
- package/src/xstate-playbook-runtime.js +63 -0
- package/src/xstate-playbook-runtime.ts +73 -0
- package/src/xstate-runtime.js +35 -4
- package/src/xstate-runtime.ts +49 -4
|
@@ -71,6 +71,7 @@ import type {
|
|
|
71
71
|
PlaybookSession,
|
|
72
72
|
PlaybookState,
|
|
73
73
|
PlaybookSuspendedCall,
|
|
74
|
+
PlaybookTerminalOutcome,
|
|
74
75
|
PlaybookTraceEvent,
|
|
75
76
|
PlaybookTraceType,
|
|
76
77
|
PlayerResult,
|
|
@@ -1980,6 +1981,50 @@ export function stateDescriptionsFromMachine(
|
|
|
1980
1981
|
return descriptions;
|
|
1981
1982
|
}
|
|
1982
1983
|
|
|
1984
|
+
/**
|
|
1985
|
+
* DR-048: each root final state's declared terminal kind, read from
|
|
1986
|
+
* `meta.playbook.terminal` in `machine.config`. The kind is compiled
|
|
1987
|
+
* metadata — the compiler derives it from the Source's own outcome wording,
|
|
1988
|
+
* exactly as it derives the state's description — so a caller learns whether
|
|
1989
|
+
* a completed child succeeded from the machine it reached, never from the
|
|
1990
|
+
* child's output fields or an agent's prose.
|
|
1991
|
+
*
|
|
1992
|
+
* A machine whose final states declare no kind yields an empty map and keeps
|
|
1993
|
+
* the pre-DR-048 delivery. A `terminal` on a non-final state, or a value
|
|
1994
|
+
* other than `success` or `failure`, is a malformed artifact and throws.
|
|
1995
|
+
*/
|
|
1996
|
+
export function terminalOutcomesFromMachine(
|
|
1997
|
+
machine: AnyStateMachine,
|
|
1998
|
+
label = 'playbook',
|
|
1999
|
+
): ReadonlyMap<string, 'success' | 'failure'> {
|
|
2000
|
+
const kinds = new Map<string, 'success' | 'failure'>();
|
|
2001
|
+
const config = (machine as unknown as { config?: unknown }).config;
|
|
2002
|
+
if (!isPlainObject(config) || !isPlainObject(config.states)) return kinds;
|
|
2003
|
+
for (const [key, stateDef] of Object.entries(config.states)) {
|
|
2004
|
+
if (!isPlainObject(stateDef)) continue;
|
|
2005
|
+
const playbook = isPlainObject(stateDef.meta)
|
|
2006
|
+
? (stateDef.meta as Record<string, unknown>).playbook
|
|
2007
|
+
: undefined;
|
|
2008
|
+
const declared = isPlainObject(playbook) ? playbook.terminal : undefined;
|
|
2009
|
+
if (declared === undefined) continue;
|
|
2010
|
+
if (declared !== 'success' && declared !== 'failure') {
|
|
2011
|
+
throw new TypeError(
|
|
2012
|
+
`${label} state ${key} declares meta.playbook.terminal ` +
|
|
2013
|
+
`${JSON.stringify(declared)}; only 'success' or 'failure' is a ` +
|
|
2014
|
+
'terminal kind',
|
|
2015
|
+
);
|
|
2016
|
+
}
|
|
2017
|
+
if (stateDef.type !== 'final') {
|
|
2018
|
+
throw new TypeError(
|
|
2019
|
+
`${label} state ${key} declares meta.playbook.terminal but is not ` +
|
|
2020
|
+
'a final state',
|
|
2021
|
+
);
|
|
2022
|
+
}
|
|
2023
|
+
kinds.set(key, declared);
|
|
2024
|
+
}
|
|
2025
|
+
return kinds;
|
|
2026
|
+
}
|
|
2027
|
+
|
|
1983
2028
|
/**
|
|
1984
2029
|
* First configured target of `eventType` from the state with `stateId`,
|
|
1985
2030
|
* falling back to the machine root's own transitions. Used only to pick the
|
|
@@ -3075,6 +3120,10 @@ export function createXStatePlaybookRuntime<
|
|
|
3075
3120
|
// DR-029: source state descriptions label the control actions the
|
|
3076
3121
|
// runtime advertises through `describe()`.
|
|
3077
3122
|
const stateDescriptions = stateDescriptionsFromMachine(machine);
|
|
3123
|
+
// DR-048: a malformed terminal declaration is a control-plane defect of the
|
|
3124
|
+
// artifact, so it fails construction rather than at the one run that
|
|
3125
|
+
// happens to reach that final state.
|
|
3126
|
+
const terminalKinds = terminalOutcomesFromMachine(machine, label);
|
|
3078
3127
|
const roleStatesDescriptor = specDescriptors.roleStates;
|
|
3079
3128
|
if (
|
|
3080
3129
|
roleStatesDescriptor !== undefined &&
|
|
@@ -6477,10 +6526,19 @@ export function createXStatePlaybookRuntime<
|
|
|
6477
6526
|
!hasUnresolvedReconciliation()
|
|
6478
6527
|
? stateDescriptionFor(state)
|
|
6479
6528
|
: undefined;
|
|
6529
|
+
// DR-048: the reached final state's compiled terminal meaning, read
|
|
6530
|
+
// from the artifact. It is withheld exactly when the published
|
|
6531
|
+
// description is, so an unresolved reconciliation publishes no
|
|
6532
|
+
// terminal meaning at all.
|
|
6533
|
+
const terminal =
|
|
6534
|
+
hasUnresolvedReconciliation() || state.stateId === undefined
|
|
6535
|
+
? undefined
|
|
6536
|
+
: terminalOutcomeFor(state.stateId, stateDescription);
|
|
6480
6537
|
return {
|
|
6481
6538
|
outcome,
|
|
6482
6539
|
state,
|
|
6483
6540
|
...(stateDescription === undefined ? {} : { stateDescription }),
|
|
6541
|
+
...(terminal === undefined ? {} : { terminal }),
|
|
6484
6542
|
...(output === undefined
|
|
6485
6543
|
? {}
|
|
6486
6544
|
: {
|
|
@@ -6867,6 +6925,21 @@ export function createXStatePlaybookRuntime<
|
|
|
6867
6925
|
return undefined;
|
|
6868
6926
|
}
|
|
6869
6927
|
|
|
6928
|
+
// DR-048: the public terminal record for a reached final state, present
|
|
6929
|
+
// only for an artifact that declares that state's kind.
|
|
6930
|
+
function terminalOutcomeFor(
|
|
6931
|
+
stateId: string,
|
|
6932
|
+
description: string | undefined,
|
|
6933
|
+
): PlaybookTerminalOutcome | undefined {
|
|
6934
|
+
const kind = terminalKinds.get(stateId);
|
|
6935
|
+
if (kind === undefined) return undefined;
|
|
6936
|
+
return {
|
|
6937
|
+
stateId,
|
|
6938
|
+
kind,
|
|
6939
|
+
...(description === undefined ? {} : { description }),
|
|
6940
|
+
};
|
|
6941
|
+
}
|
|
6942
|
+
|
|
6870
6943
|
function receiptTracePayload(
|
|
6871
6944
|
receipt: PlaybookControlReceipt,
|
|
6872
6945
|
): Record<string, unknown> {
|
package/src/xstate-runtime.js
CHANGED
|
@@ -1617,7 +1617,13 @@ export function assertPlaybookRuntimeSnapshot(value, expectedPlaybookId, options
|
|
|
1617
1617
|
export class NestedPlaybookCallError extends Error {
|
|
1618
1618
|
result;
|
|
1619
1619
|
constructor(result) {
|
|
1620
|
-
|
|
1620
|
+
// DR-048: a completed child that reached an authored failure terminal is
|
|
1621
|
+
// rejected through the same error path as an abort or an error, so its
|
|
1622
|
+
// message names that final state rather than reporting `ok`.
|
|
1623
|
+
const fallback = result.status === 'ok'
|
|
1624
|
+
? `Child playbook ${result.playbookId} reached failure terminal ` +
|
|
1625
|
+
`${result.terminal?.stateId ?? 'unknown'}`
|
|
1626
|
+
: `Child playbook ${result.playbookId} ${result.status}`;
|
|
1621
1627
|
const normalized = result.status === 'ok' ? undefined : result.error;
|
|
1622
1628
|
super(normalized?.message ?? fallback);
|
|
1623
1629
|
this.name = normalized?.name ?? 'NestedPlaybookCallError';
|
|
@@ -1723,6 +1729,26 @@ function validateNormalizedError(error, path) {
|
|
|
1723
1729
|
throw new TypeError(`${path}.stack must be a string`);
|
|
1724
1730
|
}
|
|
1725
1731
|
}
|
|
1732
|
+
// DR-048: the completed child's compiled terminal record. It is runtime-owned
|
|
1733
|
+
// data read from the child's artifact, so a malformed one is a control-plane
|
|
1734
|
+
// error rather than an authored child outcome.
|
|
1735
|
+
function validateTerminalOutcome(value) {
|
|
1736
|
+
if (!isRecord(value)) {
|
|
1737
|
+
throw new TypeError('playbook result terminal must be an object');
|
|
1738
|
+
}
|
|
1739
|
+
rejectUnknownKeys(value, ['stateId', 'kind', 'description'], 'playbook result terminal');
|
|
1740
|
+
requireNonEmptyString(value.stateId, 'playbook result terminal stateId');
|
|
1741
|
+
if (value.kind !== 'success' && value.kind !== 'failure') {
|
|
1742
|
+
throw new TypeError("playbook result terminal kind must be 'success' or 'failure'");
|
|
1743
|
+
}
|
|
1744
|
+
if (own(value, 'description') && typeof value.description !== 'string') {
|
|
1745
|
+
throw new TypeError('playbook result terminal description must be a string');
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
/** DR-048: a completed child that reached an authored failure terminal. */
|
|
1749
|
+
function isFailureTerminal(result) {
|
|
1750
|
+
return result.status === 'ok' && result.terminal?.kind === 'failure';
|
|
1751
|
+
}
|
|
1726
1752
|
export function validatePlaybookCallResult(result, expectedPlaybookId, expectedChildSessionId) {
|
|
1727
1753
|
const capturedResult = snapshotJsonValue(result, 'playbook result');
|
|
1728
1754
|
if (!isRecord(capturedResult)) {
|
|
@@ -1737,8 +1763,11 @@ export function validatePlaybookCallResult(result, expectedPlaybookId, expectedC
|
|
|
1737
1763
|
throw new PlaybookCallIdentityError(`playbook result target ${String(capturedResult.playbookId)} does not match ${expectedPlaybookId}`);
|
|
1738
1764
|
}
|
|
1739
1765
|
if (capturedResult.status === 'ok') {
|
|
1740
|
-
rejectUnknownKeys(capturedResult, ['status', 'playbookId', 'childSessionId', 'state', 'output'], 'playbook result');
|
|
1766
|
+
rejectUnknownKeys(capturedResult, ['status', 'playbookId', 'childSessionId', 'state', 'output', 'terminal'], 'playbook result');
|
|
1741
1767
|
requireNonEmptyString(capturedResult.childSessionId, 'playbook result childSessionId');
|
|
1768
|
+
if (own(capturedResult, 'terminal')) {
|
|
1769
|
+
validateTerminalOutcome(capturedResult.terminal);
|
|
1770
|
+
}
|
|
1742
1771
|
}
|
|
1743
1772
|
else {
|
|
1744
1773
|
rejectUnknownKeys(capturedResult, ['status', 'playbookId', 'childSessionId', 'state', 'error'], 'playbook result');
|
|
@@ -1832,8 +1861,9 @@ function resultFromThrown(playbookId, childSessionId, error, aborted) {
|
|
|
1832
1861
|
return snapshotJsonValue(result, 'playbook result');
|
|
1833
1862
|
}
|
|
1834
1863
|
function outputOrThrow(result) {
|
|
1835
|
-
if (result.status === 'ok')
|
|
1864
|
+
if (result.status === 'ok' && !isFailureTerminal(result)) {
|
|
1836
1865
|
return result.output;
|
|
1866
|
+
}
|
|
1837
1867
|
throw new NestedPlaybookCallError(result);
|
|
1838
1868
|
}
|
|
1839
1869
|
export function createNestedPlaybookBridge(options) {
|
|
@@ -2014,7 +2044,8 @@ export function createNestedPlaybookBridge(options) {
|
|
|
2014
2044
|
else if (cleanupControlError !== undefined) {
|
|
2015
2045
|
active.deferred.reject(cleanupControlError);
|
|
2016
2046
|
}
|
|
2017
|
-
else if (effectiveResult.status === 'ok'
|
|
2047
|
+
else if (effectiveResult.status === 'ok' &&
|
|
2048
|
+
!isFailureTerminal(effectiveResult)) {
|
|
2018
2049
|
active.deferred.resolve(effectiveResult.output);
|
|
2019
2050
|
}
|
|
2020
2051
|
else {
|
package/src/xstate-runtime.ts
CHANGED
|
@@ -2596,7 +2596,14 @@ export class NestedPlaybookCallError extends Error {
|
|
|
2596
2596
|
readonly result: PlaybookCallResult;
|
|
2597
2597
|
|
|
2598
2598
|
constructor(result: PlaybookCallResult) {
|
|
2599
|
-
|
|
2599
|
+
// DR-048: a completed child that reached an authored failure terminal is
|
|
2600
|
+
// rejected through the same error path as an abort or an error, so its
|
|
2601
|
+
// message names that final state rather than reporting `ok`.
|
|
2602
|
+
const fallback =
|
|
2603
|
+
result.status === 'ok'
|
|
2604
|
+
? `Child playbook ${result.playbookId} reached failure terminal ` +
|
|
2605
|
+
`${result.terminal?.stateId ?? 'unknown'}`
|
|
2606
|
+
: `Child playbook ${result.playbookId} ${result.status}`;
|
|
2600
2607
|
const normalized = result.status === 'ok' ? undefined : result.error;
|
|
2601
2608
|
super(normalized?.message ?? fallback);
|
|
2602
2609
|
this.name = normalized?.name ?? 'NestedPlaybookCallError';
|
|
@@ -2794,6 +2801,36 @@ function validateNormalizedError(error: unknown, path: string): void {
|
|
|
2794
2801
|
}
|
|
2795
2802
|
}
|
|
2796
2803
|
|
|
2804
|
+
// DR-048: the completed child's compiled terminal record. It is runtime-owned
|
|
2805
|
+
// data read from the child's artifact, so a malformed one is a control-plane
|
|
2806
|
+
// error rather than an authored child outcome.
|
|
2807
|
+
function validateTerminalOutcome(value: unknown): void {
|
|
2808
|
+
if (!isRecord(value)) {
|
|
2809
|
+
throw new TypeError('playbook result terminal must be an object');
|
|
2810
|
+
}
|
|
2811
|
+
rejectUnknownKeys(
|
|
2812
|
+
value,
|
|
2813
|
+
['stateId', 'kind', 'description'],
|
|
2814
|
+
'playbook result terminal',
|
|
2815
|
+
);
|
|
2816
|
+
requireNonEmptyString(value.stateId, 'playbook result terminal stateId');
|
|
2817
|
+
if (value.kind !== 'success' && value.kind !== 'failure') {
|
|
2818
|
+
throw new TypeError(
|
|
2819
|
+
"playbook result terminal kind must be 'success' or 'failure'",
|
|
2820
|
+
);
|
|
2821
|
+
}
|
|
2822
|
+
if (own(value, 'description') && typeof value.description !== 'string') {
|
|
2823
|
+
throw new TypeError(
|
|
2824
|
+
'playbook result terminal description must be a string',
|
|
2825
|
+
);
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2829
|
+
/** DR-048: a completed child that reached an authored failure terminal. */
|
|
2830
|
+
function isFailureTerminal(result: PlaybookCallResult): boolean {
|
|
2831
|
+
return result.status === 'ok' && result.terminal?.kind === 'failure';
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2797
2834
|
export function validatePlaybookCallResult(
|
|
2798
2835
|
result: unknown,
|
|
2799
2836
|
expectedPlaybookId: string,
|
|
@@ -2818,13 +2855,16 @@ export function validatePlaybookCallResult(
|
|
|
2818
2855
|
if (capturedResult.status === 'ok') {
|
|
2819
2856
|
rejectUnknownKeys(
|
|
2820
2857
|
capturedResult,
|
|
2821
|
-
['status', 'playbookId', 'childSessionId', 'state', 'output'],
|
|
2858
|
+
['status', 'playbookId', 'childSessionId', 'state', 'output', 'terminal'],
|
|
2822
2859
|
'playbook result',
|
|
2823
2860
|
);
|
|
2824
2861
|
requireNonEmptyString(
|
|
2825
2862
|
capturedResult.childSessionId,
|
|
2826
2863
|
'playbook result childSessionId',
|
|
2827
2864
|
);
|
|
2865
|
+
if (own(capturedResult, 'terminal')) {
|
|
2866
|
+
validateTerminalOutcome(capturedResult.terminal);
|
|
2867
|
+
}
|
|
2828
2868
|
} else {
|
|
2829
2869
|
rejectUnknownKeys(
|
|
2830
2870
|
capturedResult,
|
|
@@ -2973,7 +3013,9 @@ function resultFromThrown(
|
|
|
2973
3013
|
}
|
|
2974
3014
|
|
|
2975
3015
|
function outputOrThrow(result: PlaybookCallResult): JsonValue | undefined {
|
|
2976
|
-
if (result.status === 'ok'
|
|
3016
|
+
if (result.status === 'ok' && !isFailureTerminal(result)) {
|
|
3017
|
+
return result.output;
|
|
3018
|
+
}
|
|
2977
3019
|
throw new NestedPlaybookCallError(result);
|
|
2978
3020
|
}
|
|
2979
3021
|
|
|
@@ -3206,7 +3248,10 @@ export function createNestedPlaybookBridge<
|
|
|
3206
3248
|
active.deferred.reject(controlError);
|
|
3207
3249
|
} else if (cleanupControlError !== undefined) {
|
|
3208
3250
|
active.deferred.reject(cleanupControlError);
|
|
3209
|
-
} else if (
|
|
3251
|
+
} else if (
|
|
3252
|
+
effectiveResult.status === 'ok' &&
|
|
3253
|
+
!isFailureTerminal(effectiveResult)
|
|
3254
|
+
) {
|
|
3210
3255
|
active.deferred.resolve(effectiveResult.output);
|
|
3211
3256
|
} else {
|
|
3212
3257
|
active.deferred.reject(new NestedPlaybookCallError(effectiveResult));
|