@sublang/playbook 14.0.0 → 14.1.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/cli.md +14 -14
- package/package.json +1 -1
- package/reference/sdlc/branch.playbook/branch.fsm.d.ts +1 -1
- package/reference/sdlc/code.playbook/bin/repository-effects.js +187 -14
- package/reference/sdlc/code.playbook/code.fsm.d.ts +1 -1
- package/reference/sdlc/code.playbook/host-capabilities.d.ts +6 -0
- package/reference/sdlc/code.playbook/playbook-captain.d.ts +3 -1
- package/reference/sdlc/code.playbook/playbook-captain.js +357 -16
- package/reference/sdlc/code.playbook/playbook-captain.ts +459 -34
- package/reference/sdlc/code.playbook/session-host.d.ts +6 -3
- package/reference/sdlc/decide.playbook/decide.fsm.d.ts +1 -1
- package/reference/sdlc/decide.playbook/decide.playbook.js +25 -1
- package/reference/sdlc/decide.playbook/decide.playbook.ts +35 -2
- package/reference/sdlc/dev.md +15 -8
- package/reference/sdlc/dev.playbook/dev.fsm.d.ts +1 -1
- package/reference/sdlc/dev.playbook/dev.fsm.js +18 -12
- package/reference/sdlc/dev.playbook/dev.fsm.ts +18 -13
- package/reference/sdlc/dev.playbook/dev.gears.md +19 -12
- package/reference/sdlc/dev.playbook/dev.playbook.js +1 -1
- package/reference/sdlc/dev.playbook/dev.playbook.ts +1 -2
- package/reference/sdlc/dev.playbook/dev.registry.js +1 -1
- package/reference/sdlc/dev.playbook/dev.registry.ts +1 -1
- package/reference/sdlc/pr.playbook/pr.fsm.d.ts +1 -1
- package/reference/sdlc/review.playbook/review.fsm.d.ts +1 -1
- package/slc/link.md +32 -0
- package/src/runtime.d.ts +70 -0
- package/src/runtime.js +232 -1
- package/src/runtime.ts +358 -1
- package/src/xstate-playbook-runtime.d.ts +9 -10
- package/src/xstate-playbook-runtime.js +372 -20
- package/src/xstate-playbook-runtime.ts +460 -19
- package/src/xstate-runtime.d.ts +9 -1
- package/src/xstate-runtime.js +299 -19
- package/src/xstate-runtime.ts +362 -21
package/src/xstate-runtime.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
|
|
3
3
|
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
5
6
|
import {
|
|
6
7
|
fromPromise,
|
|
7
8
|
waitFor,
|
|
@@ -10,10 +11,13 @@ import {
|
|
|
10
11
|
type SnapshotFrom,
|
|
11
12
|
} from 'xstate';
|
|
12
13
|
|
|
14
|
+
import { assertPlaybookFailureCause } from './runtime.js';
|
|
13
15
|
import type {
|
|
14
16
|
CaptainResult,
|
|
15
17
|
JsonValue,
|
|
16
18
|
NormalizedError,
|
|
19
|
+
PlaybookFailureCause,
|
|
20
|
+
PlaybookFailurePaths,
|
|
17
21
|
PlaybookCallRequest,
|
|
18
22
|
PlaybookCallResult,
|
|
19
23
|
PlaybookCallStart,
|
|
@@ -634,7 +638,74 @@ export function hiddenControlEnvelope(prompt: string): string {
|
|
|
634
638
|
].join('\n\n');
|
|
635
639
|
}
|
|
636
640
|
|
|
641
|
+
/**
|
|
642
|
+
* DR-063 §2: the marker property the runtime writes on the error it marks as
|
|
643
|
+
* the FSM failure. It is non-enumerable, so decorating a host-owned error
|
|
644
|
+
* changes nothing a host can observe by enumeration or serialization.
|
|
645
|
+
*/
|
|
646
|
+
const PLAYBOOK_FAILURE_CAUSE_PROPERTY = 'playbookCause';
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Attach one validated failure cause to an error object, returning it. A cause
|
|
650
|
+
* the closed validator rejects is dropped rather than published, and an error
|
|
651
|
+
* that refuses the property keeps its own cause-free identity (DR-063 §1).
|
|
652
|
+
*/
|
|
653
|
+
export function attachPlaybookFailureCause<E>(error: E, cause: unknown): E {
|
|
654
|
+
if (typeof error !== 'object' || error === null) return error;
|
|
655
|
+
let validated: PlaybookFailureCause;
|
|
656
|
+
try {
|
|
657
|
+
validated = assertPlaybookFailureCause(cause);
|
|
658
|
+
} catch {
|
|
659
|
+
return error;
|
|
660
|
+
}
|
|
661
|
+
try {
|
|
662
|
+
Object.defineProperty(error, PLAYBOOK_FAILURE_CAUSE_PROPERTY, {
|
|
663
|
+
value: validated,
|
|
664
|
+
enumerable: false,
|
|
665
|
+
writable: true,
|
|
666
|
+
configurable: true,
|
|
667
|
+
});
|
|
668
|
+
} catch {
|
|
669
|
+
// A frozen or hostile error keeps no cause; the runtime's own slot still
|
|
670
|
+
// supplies one at the failed state.
|
|
671
|
+
}
|
|
672
|
+
return error;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* DR-063 §2: a normalized error carries `cause` exactly when the underlying
|
|
677
|
+
* error carries a valid one. An `Error` carries it on the marker property the
|
|
678
|
+
* runtime writes; an already-normalized record carries it as `cause`, so the
|
|
679
|
+
* cause survives every re-normalization along the durable round trip.
|
|
680
|
+
*/
|
|
681
|
+
function playbookFailureCauseOf(
|
|
682
|
+
error: unknown,
|
|
683
|
+
): PlaybookFailureCause | undefined {
|
|
684
|
+
if (typeof error !== 'object' || error === null) return undefined;
|
|
685
|
+
let candidate: unknown;
|
|
686
|
+
try {
|
|
687
|
+
if (Object.hasOwn(error, PLAYBOOK_FAILURE_CAUSE_PROPERTY)) {
|
|
688
|
+
candidate = (error as Record<string, unknown>)[
|
|
689
|
+
PLAYBOOK_FAILURE_CAUSE_PROPERTY
|
|
690
|
+
];
|
|
691
|
+
} else if (!(error instanceof Error) && Object.hasOwn(error, 'cause')) {
|
|
692
|
+
candidate = (error as Record<string, unknown>).cause;
|
|
693
|
+
}
|
|
694
|
+
} catch {
|
|
695
|
+
return undefined;
|
|
696
|
+
}
|
|
697
|
+
if (candidate === undefined) return undefined;
|
|
698
|
+
try {
|
|
699
|
+
return assertPlaybookFailureCause(candidate);
|
|
700
|
+
} catch {
|
|
701
|
+
return undefined;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
637
705
|
export function normalizeError(error: unknown): NormalizedError {
|
|
706
|
+
const cause = playbookFailureCauseOf(error);
|
|
707
|
+
const withCause = (normalized: NormalizedError): NormalizedError =>
|
|
708
|
+
cause === undefined ? normalized : { ...normalized, cause };
|
|
638
709
|
if (error instanceof Error) {
|
|
639
710
|
let name = 'Error';
|
|
640
711
|
let message = 'Unknown error';
|
|
@@ -656,11 +727,11 @@ export function normalizeError(error: unknown): NormalizedError {
|
|
|
656
727
|
} catch {
|
|
657
728
|
// A stack is optional at the public boundary.
|
|
658
729
|
}
|
|
659
|
-
return {
|
|
730
|
+
return withCause({
|
|
660
731
|
name,
|
|
661
732
|
message,
|
|
662
733
|
...(stack ? { stack } : {}),
|
|
663
|
-
};
|
|
734
|
+
});
|
|
664
735
|
}
|
|
665
736
|
if (typeof error === 'string') {
|
|
666
737
|
return { name: 'Error', message: error };
|
|
@@ -668,21 +739,21 @@ export function normalizeError(error: unknown): NormalizedError {
|
|
|
668
739
|
try {
|
|
669
740
|
assertJsonSafe(error);
|
|
670
741
|
if (isRecord(error) && typeof error.message === 'string') {
|
|
671
|
-
return {
|
|
742
|
+
return withCause({
|
|
672
743
|
name:
|
|
673
744
|
typeof error.name === 'string' && error.name.length > 0
|
|
674
745
|
? error.name
|
|
675
746
|
: 'Error',
|
|
676
747
|
message: error.message,
|
|
677
748
|
...(typeof error.stack === 'string' ? { stack: error.stack } : {}),
|
|
678
|
-
};
|
|
749
|
+
});
|
|
679
750
|
}
|
|
680
|
-
return { name: 'Error', message: JSON.stringify(error) };
|
|
751
|
+
return withCause({ name: 'Error', message: JSON.stringify(error) });
|
|
681
752
|
} catch {
|
|
682
753
|
try {
|
|
683
|
-
return { name: 'Error', message: String(error) };
|
|
754
|
+
return withCause({ name: 'Error', message: String(error) });
|
|
684
755
|
} catch {
|
|
685
|
-
return { name: 'Error', message: 'Unknown error' };
|
|
756
|
+
return withCause({ name: 'Error', message: 'Unknown error' });
|
|
686
757
|
}
|
|
687
758
|
}
|
|
688
759
|
}
|
|
@@ -951,17 +1022,76 @@ function projectionsEqual(
|
|
|
951
1022
|
);
|
|
952
1023
|
}
|
|
953
1024
|
|
|
954
|
-
function
|
|
955
|
-
|
|
956
|
-
|
|
1025
|
+
function projectionEntriesEqual(
|
|
1026
|
+
left: PlaybookRepositoryObservation,
|
|
1027
|
+
right: PlaybookRepositoryObservation,
|
|
1028
|
+
path: string,
|
|
957
1029
|
): boolean {
|
|
958
|
-
return
|
|
959
|
-
(
|
|
960
|
-
|
|
961
|
-
JSON.stringify(
|
|
1030
|
+
return (
|
|
1031
|
+
own(right.projection, path) &&
|
|
1032
|
+
JSON.stringify(left.projection[path]) ===
|
|
1033
|
+
JSON.stringify(right.projection[path])
|
|
962
1034
|
);
|
|
963
1035
|
}
|
|
964
1036
|
|
|
1037
|
+
const EFFECT_PRE_EXISTING_LISTS = ['absorbed', 'altered', 'lost'] as const;
|
|
1038
|
+
|
|
1039
|
+
const EMPTY_PRE_EXISTING: Readonly<
|
|
1040
|
+
Record<(typeof EFFECT_PRE_EXISTING_LISTS)[number], readonly string[]>
|
|
1041
|
+
> = Object.freeze({
|
|
1042
|
+
absorbed: Object.freeze([]),
|
|
1043
|
+
altered: Object.freeze([]),
|
|
1044
|
+
lost: Object.freeze([]),
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
/**
|
|
1048
|
+
* The pre-existing accounting a physical or logical receipt records
|
|
1049
|
+
* (DR-062 §3): sorted unique baseline paths, pairwise disjoint, present
|
|
1050
|
+
* exactly when one list is nonempty.
|
|
1051
|
+
*/
|
|
1052
|
+
function effectPreExisting(
|
|
1053
|
+
value: unknown,
|
|
1054
|
+
path: string,
|
|
1055
|
+
baseline: PlaybookRepositoryObservation,
|
|
1056
|
+
): Readonly<Record<(typeof EFFECT_PRE_EXISTING_LISTS)[number], readonly string[]>> {
|
|
1057
|
+
if (!isRecord(value)) throw new TypeError(`${path} must be an object`);
|
|
1058
|
+
rejectUnknownKeys(value, EFFECT_PRE_EXISTING_LISTS, path);
|
|
1059
|
+
const claimed = new Set<string>();
|
|
1060
|
+
const lists = EFFECT_PRE_EXISTING_LISTS.map((name) => {
|
|
1061
|
+
const entries = value[name];
|
|
1062
|
+
if (!Array.isArray(entries)) {
|
|
1063
|
+
throw new TypeError(`${path}.${name} must be an array of baseline paths`);
|
|
1064
|
+
}
|
|
1065
|
+
let previous: string | undefined;
|
|
1066
|
+
for (const entry of entries) {
|
|
1067
|
+
if (typeof entry !== 'string' || entry.length === 0) {
|
|
1068
|
+
throw new TypeError(`${path}.${name} must hold nonempty baseline paths`);
|
|
1069
|
+
}
|
|
1070
|
+
const member = entry;
|
|
1071
|
+
if (previous !== undefined && member <= previous) {
|
|
1072
|
+
throw new TypeError(`${path}.${name} must be sorted and unique`);
|
|
1073
|
+
}
|
|
1074
|
+
previous = member;
|
|
1075
|
+
if (!own(baseline.projection, member)) {
|
|
1076
|
+
throw new TypeError(
|
|
1077
|
+
`${path}.${name} names a path outside the baseline projection`,
|
|
1078
|
+
);
|
|
1079
|
+
}
|
|
1080
|
+
if (claimed.has(member)) {
|
|
1081
|
+
throw new TypeError(`${path} lists ${JSON.stringify(member)} twice`);
|
|
1082
|
+
}
|
|
1083
|
+
claimed.add(member);
|
|
1084
|
+
}
|
|
1085
|
+
return [name, entries as readonly string[]] as const;
|
|
1086
|
+
});
|
|
1087
|
+
if (claimed.size === 0) {
|
|
1088
|
+
throw new TypeError(`${path} must name at least one baseline path`);
|
|
1089
|
+
}
|
|
1090
|
+
return Object.fromEntries(lists) as Readonly<
|
|
1091
|
+
Record<(typeof EFFECT_PRE_EXISTING_LISTS)[number], readonly string[]>
|
|
1092
|
+
>;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
965
1095
|
function effectObservation(
|
|
966
1096
|
value: unknown,
|
|
967
1097
|
path: string,
|
|
@@ -1026,7 +1156,7 @@ function effectReceipt(
|
|
|
1026
1156
|
if (!isRecord(value)) throw new TypeError(`${path} must be an object`);
|
|
1027
1157
|
rejectUnknownKeys(
|
|
1028
1158
|
value,
|
|
1029
|
-
['classification', 'baseline', 'after', 'commitOid'],
|
|
1159
|
+
['classification', 'baseline', 'after', 'commitOid', 'preExisting'],
|
|
1030
1160
|
path,
|
|
1031
1161
|
);
|
|
1032
1162
|
if (
|
|
@@ -1096,14 +1226,40 @@ function effectReceipt(
|
|
|
1096
1226
|
) {
|
|
1097
1227
|
throw new TypeError(`${path} classified unchanged observations that differ`);
|
|
1098
1228
|
}
|
|
1229
|
+
// DR-062 §3: a receipt records the fate of every baseline entry, so the
|
|
1230
|
+
// accounting and the two observations must agree exactly.
|
|
1231
|
+
if (
|
|
1232
|
+
own(value, 'preExisting') &&
|
|
1233
|
+
classification !== 'one-descendant-commit' &&
|
|
1234
|
+
classification !== 'worktree-only-change' &&
|
|
1235
|
+
classification !== 'observation-ambiguous'
|
|
1236
|
+
) {
|
|
1237
|
+
throw new TypeError(
|
|
1238
|
+
`${path}.preExisting is not permitted for ${classification}`,
|
|
1239
|
+
);
|
|
1240
|
+
}
|
|
1241
|
+
const preExisting = own(value, 'preExisting')
|
|
1242
|
+
? effectPreExisting(value.preExisting, `${path}.preExisting`, baseline)
|
|
1243
|
+
: EMPTY_PRE_EXISTING;
|
|
1244
|
+
const consumed = new Set([...preExisting.absorbed, ...preExisting.altered]);
|
|
1099
1245
|
if (
|
|
1100
1246
|
classification === 'one-descendant-commit' &&
|
|
1101
1247
|
after !== undefined &&
|
|
1102
1248
|
(baseline.head === after.head ||
|
|
1103
|
-
|
|
1249
|
+
preExisting.lost.length > 0 ||
|
|
1250
|
+
!Object.keys(after.projection).every(
|
|
1251
|
+
(member) =>
|
|
1252
|
+
own(baseline.projection, member) &&
|
|
1253
|
+
projectionEntriesEqual(baseline, after, member),
|
|
1254
|
+
) ||
|
|
1255
|
+
!Object.keys(baseline.projection).every(
|
|
1256
|
+
(member) =>
|
|
1257
|
+
own(after.projection, member) !== consumed.has(member),
|
|
1258
|
+
) ||
|
|
1259
|
+
(consumed.size === 0) !== projectionsEqual(baseline, after))
|
|
1104
1260
|
) {
|
|
1105
1261
|
throw new TypeError(
|
|
1106
|
-
`${path} one-descendant-commit must change HEAD and
|
|
1262
|
+
`${path} one-descendant-commit must change HEAD and account for its complete baseline projection`,
|
|
1107
1263
|
);
|
|
1108
1264
|
}
|
|
1109
1265
|
if (
|
|
@@ -1111,10 +1267,17 @@ function effectReceipt(
|
|
|
1111
1267
|
after !== undefined &&
|
|
1112
1268
|
(baseline.head !== after.head ||
|
|
1113
1269
|
projectionsEqual(baseline, after) ||
|
|
1114
|
-
|
|
1270
|
+
preExisting.absorbed.length > 0 ||
|
|
1271
|
+
preExisting.lost.length > 0 ||
|
|
1272
|
+
!Object.keys(baseline.projection).every(
|
|
1273
|
+
(member) =>
|
|
1274
|
+
own(after.projection, member) &&
|
|
1275
|
+
projectionEntriesEqual(baseline, after, member) !==
|
|
1276
|
+
consumed.has(member),
|
|
1277
|
+
))
|
|
1115
1278
|
) {
|
|
1116
1279
|
throw new TypeError(
|
|
1117
|
-
`${path} worktree-only-change must preserve HEAD
|
|
1280
|
+
`${path} worktree-only-change must preserve HEAD, change the projection, and account for every altered baseline entry`,
|
|
1118
1281
|
);
|
|
1119
1282
|
}
|
|
1120
1283
|
if (
|
|
@@ -1215,6 +1378,8 @@ export type PlaybookSemanticReconciliation =
|
|
|
1215
1378
|
| {
|
|
1216
1379
|
readonly status: 'unresolved';
|
|
1217
1380
|
readonly reason: PlaybookSemanticReconciliationReason;
|
|
1381
|
+
/** DR-063 §1: why it stays unresolved, as a closed structured cause. */
|
|
1382
|
+
readonly cause: PlaybookFailureCause;
|
|
1218
1383
|
readonly evidence: PlaybookRetainedSemanticEvidence;
|
|
1219
1384
|
};
|
|
1220
1385
|
|
|
@@ -1302,11 +1467,172 @@ function retainedSemanticEvidence(
|
|
|
1302
1467
|
});
|
|
1303
1468
|
}
|
|
1304
1469
|
|
|
1470
|
+
// ---------------------------------------------------------------------------
|
|
1471
|
+
// DR-063 §1: the evidence builder for a repository-decided failure. Every list
|
|
1472
|
+
// is sorted, unique, and bounded; `truncated` carries the count the bound
|
|
1473
|
+
// omitted. Nothing but repository paths, dispositions, classifications, and
|
|
1474
|
+
// revision identities enters a cause.
|
|
1475
|
+
// ---------------------------------------------------------------------------
|
|
1476
|
+
|
|
1477
|
+
const FAILURE_PATH_BOUND = 32;
|
|
1478
|
+
|
|
1479
|
+
function boundedFailurePaths(
|
|
1480
|
+
lists: Readonly<Record<string, readonly string[]>>,
|
|
1481
|
+
): PlaybookFailurePaths {
|
|
1482
|
+
const bounded: Record<string, unknown> = {};
|
|
1483
|
+
let omitted = 0;
|
|
1484
|
+
for (const [member, paths] of Object.entries(lists)) {
|
|
1485
|
+
bounded[member] = paths.slice(0, FAILURE_PATH_BOUND);
|
|
1486
|
+
omitted += Math.max(0, paths.length - FAILURE_PATH_BOUND);
|
|
1487
|
+
}
|
|
1488
|
+
if (omitted > 0) bounded.truncated = omitted;
|
|
1489
|
+
return bounded as PlaybookFailurePaths;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
function sortedUniquePaths(paths: Iterable<string>): readonly string[] {
|
|
1493
|
+
return [...new Set(paths)].sort();
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
/** After-projection entries the baseline did not hold, or holds differently. */
|
|
1497
|
+
function newOrChangedProjectionPaths(
|
|
1498
|
+
baseline: PlaybookRepositoryObservation,
|
|
1499
|
+
after: PlaybookRepositoryObservation,
|
|
1500
|
+
): readonly string[] {
|
|
1501
|
+
return sortedUniquePaths(
|
|
1502
|
+
Object.keys(after.projection).filter(
|
|
1503
|
+
(path) =>
|
|
1504
|
+
!Object.hasOwn(baseline.projection, path) ||
|
|
1505
|
+
!isDeepStrictEqual(baseline.projection[path], after.projection[path]),
|
|
1506
|
+
),
|
|
1507
|
+
);
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
/** Entries that differ or are missing on either side of the two projections. */
|
|
1511
|
+
function divergentProjectionPaths(
|
|
1512
|
+
baseline: PlaybookRepositoryObservation,
|
|
1513
|
+
after: PlaybookRepositoryObservation,
|
|
1514
|
+
): readonly string[] {
|
|
1515
|
+
return sortedUniquePaths(
|
|
1516
|
+
[
|
|
1517
|
+
...Object.keys(baseline.projection),
|
|
1518
|
+
...Object.keys(after.projection),
|
|
1519
|
+
].filter(
|
|
1520
|
+
(path) =>
|
|
1521
|
+
!Object.hasOwn(baseline.projection, path) ||
|
|
1522
|
+
!Object.hasOwn(after.projection, path) ||
|
|
1523
|
+
!isDeepStrictEqual(baseline.projection[path], after.projection[path]),
|
|
1524
|
+
),
|
|
1525
|
+
);
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
/**
|
|
1529
|
+
* DR-063 §1: the cause of an unresolved repository-disposition mismatch, read
|
|
1530
|
+
* from the disposition the accepted arm required and the receipt the host
|
|
1531
|
+
* proved. The receipt states the failure; nothing here infers one.
|
|
1532
|
+
*/
|
|
1533
|
+
function playbookRepositoryFailureCause(
|
|
1534
|
+
required: PlaybookRepositoryDisposition,
|
|
1535
|
+
receipt: PlaybookRepositoryReceipt,
|
|
1536
|
+
): PlaybookFailureCause {
|
|
1537
|
+
const observed = receipt.classification;
|
|
1538
|
+
const baselineHead = receipt.baseline.head;
|
|
1539
|
+
const after = receipt.after;
|
|
1540
|
+
const common = { required, observed, baselineHead };
|
|
1541
|
+
const ambiguous = (): PlaybookFailureCause =>
|
|
1542
|
+
assertPlaybookFailureCause({
|
|
1543
|
+
code: 'attribution-ambiguous',
|
|
1544
|
+
evidence: {
|
|
1545
|
+
...common,
|
|
1546
|
+
...(after === undefined ? {} : { afterHead: after.head }),
|
|
1547
|
+
},
|
|
1548
|
+
});
|
|
1549
|
+
const lost = receipt.preExisting?.lost ?? [];
|
|
1550
|
+
if (lost.length > 0) {
|
|
1551
|
+
return assertPlaybookFailureCause({
|
|
1552
|
+
code: 'pre-existing-lost',
|
|
1553
|
+
evidence: {
|
|
1554
|
+
...common,
|
|
1555
|
+
...(after === undefined ? {} : { afterHead: after.head }),
|
|
1556
|
+
paths: boundedFailurePaths({ lost: sortedUniquePaths(lost) }),
|
|
1557
|
+
},
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1560
|
+
if (observed === 'observation-ambiguous' && after === undefined) {
|
|
1561
|
+
return assertPlaybookFailureCause({
|
|
1562
|
+
code: 'observation-unstable',
|
|
1563
|
+
evidence: common,
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
if (after === undefined) return ambiguous();
|
|
1567
|
+
const afterHead = after.head;
|
|
1568
|
+
if (observed === 'multiple-commits') {
|
|
1569
|
+
return assertPlaybookFailureCause({
|
|
1570
|
+
code: 'commits-more-than-one',
|
|
1571
|
+
evidence: { ...common, afterHead },
|
|
1572
|
+
});
|
|
1573
|
+
}
|
|
1574
|
+
if (observed === 'rewritten-or-non-descendant') {
|
|
1575
|
+
return assertPlaybookFailureCause({
|
|
1576
|
+
code: 'history-rewritten',
|
|
1577
|
+
evidence: { ...common, afterHead },
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1580
|
+
if (observed === 'concurrent-or-foreign-change') {
|
|
1581
|
+
return assertPlaybookFailureCause({
|
|
1582
|
+
code: 'foreign-change',
|
|
1583
|
+
evidence: {
|
|
1584
|
+
...common,
|
|
1585
|
+
afterHead,
|
|
1586
|
+
paths: boundedFailurePaths({
|
|
1587
|
+
changed: divergentProjectionPaths(receipt.baseline, after),
|
|
1588
|
+
}),
|
|
1589
|
+
},
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
const uncommitted = newOrChangedProjectionPaths(receipt.baseline, after);
|
|
1593
|
+
const altered = sortedUniquePaths(receipt.preExisting?.altered ?? []);
|
|
1594
|
+
if (
|
|
1595
|
+
observed === 'observation-ambiguous' &&
|
|
1596
|
+
afterHead !== baselineHead &&
|
|
1597
|
+
(uncommitted.length > 0 || altered.length > 0)
|
|
1598
|
+
) {
|
|
1599
|
+
return assertPlaybookFailureCause({
|
|
1600
|
+
code: 'commit-residual',
|
|
1601
|
+
evidence: {
|
|
1602
|
+
...common,
|
|
1603
|
+
afterHead,
|
|
1604
|
+
...(receipt.commitOid === undefined
|
|
1605
|
+
? {}
|
|
1606
|
+
: { commitOid: receipt.commitOid }),
|
|
1607
|
+
paths: boundedFailurePaths({ uncommitted, altered }),
|
|
1608
|
+
},
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1611
|
+
if (
|
|
1612
|
+
required === 'one-descendant-commit' &&
|
|
1613
|
+
(observed === 'unchanged' || observed === 'worktree-only-change')
|
|
1614
|
+
) {
|
|
1615
|
+
return assertPlaybookFailureCause({
|
|
1616
|
+
code: 'commit-missing',
|
|
1617
|
+
evidence: {
|
|
1618
|
+
...common,
|
|
1619
|
+
afterHead,
|
|
1620
|
+
paths: boundedFailurePaths({ uncommitted }),
|
|
1621
|
+
},
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
return ambiguous();
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1305
1627
|
function unresolvedSemanticEvidence(
|
|
1306
1628
|
reason: PlaybookSemanticReconciliationReason,
|
|
1307
1629
|
evidence: PlaybookRetainedSemanticEvidence,
|
|
1630
|
+
cause: PlaybookFailureCause = assertPlaybookFailureCause({
|
|
1631
|
+
code: 'runtime-defect',
|
|
1632
|
+
evidence: { reason },
|
|
1633
|
+
}),
|
|
1308
1634
|
): PlaybookSemanticReconciliation {
|
|
1309
|
-
return Object.freeze({ status: 'unresolved', reason, evidence });
|
|
1635
|
+
return Object.freeze({ status: 'unresolved', reason, cause, evidence });
|
|
1310
1636
|
}
|
|
1311
1637
|
|
|
1312
1638
|
/**
|
|
@@ -1372,6 +1698,7 @@ export function reconcilePlaybookSemanticEvidence(
|
|
|
1372
1698
|
return unresolvedSemanticEvidence(
|
|
1373
1699
|
'repository-disposition-mismatch',
|
|
1374
1700
|
evidence,
|
|
1701
|
+
playbookRepositoryFailureCause(disposition, receipt),
|
|
1375
1702
|
);
|
|
1376
1703
|
}
|
|
1377
1704
|
|
|
@@ -2609,6 +2936,17 @@ export class NestedPlaybookCallError extends Error {
|
|
|
2609
2936
|
this.name = normalized?.name ?? 'NestedPlaybookCallError';
|
|
2610
2937
|
if (normalized?.stack) this.stack = normalized.stack;
|
|
2611
2938
|
this.result = result;
|
|
2939
|
+
// DR-063 §2: the child's failure reaching its parent is `child-failed`,
|
|
2940
|
+
// carrying the child's own cause when its normalized error published one.
|
|
2941
|
+
attachPlaybookFailureCause(this, {
|
|
2942
|
+
code: 'child-failed',
|
|
2943
|
+
evidence: {
|
|
2944
|
+
playbookId: result.playbookId,
|
|
2945
|
+
...(normalized?.cause === undefined
|
|
2946
|
+
? {}
|
|
2947
|
+
: { cause: normalized.cause }),
|
|
2948
|
+
},
|
|
2949
|
+
});
|
|
2612
2950
|
}
|
|
2613
2951
|
}
|
|
2614
2952
|
|
|
@@ -2791,7 +3129,7 @@ function validateNormalizedError(error: unknown, path: string): void {
|
|
|
2791
3129
|
if (!isRecord(error)) {
|
|
2792
3130
|
throw new TypeError(`${path} must be a normalized error`);
|
|
2793
3131
|
}
|
|
2794
|
-
rejectUnknownKeys(error, ['name', 'message', 'stack'], path);
|
|
3132
|
+
rejectUnknownKeys(error, ['name', 'message', 'stack', 'cause'], path);
|
|
2795
3133
|
requireNonEmptyString(error.name, `${path}.name`);
|
|
2796
3134
|
if (typeof error.message !== 'string') {
|
|
2797
3135
|
throw new TypeError(`${path}.message must be a string`);
|
|
@@ -2799,6 +3137,9 @@ function validateNormalizedError(error: unknown, path: string): void {
|
|
|
2799
3137
|
if (error.stack !== undefined && typeof error.stack !== 'string') {
|
|
2800
3138
|
throw new TypeError(`${path}.stack must be a string`);
|
|
2801
3139
|
}
|
|
3140
|
+
// DR-063 §2: a normalized error may carry its structured cause, and an
|
|
3141
|
+
// unrecognized shape is not a cause.
|
|
3142
|
+
if (error.cause !== undefined) assertPlaybookFailureCause(error.cause);
|
|
2802
3143
|
}
|
|
2803
3144
|
|
|
2804
3145
|
// DR-048: the completed child's compiled terminal record. It is runtime-owned
|