@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.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type AnyActorRef, type PromiseActorLogic, type SnapshotFrom } from 'xstate';
|
|
2
|
-
import type { CaptainResult, JsonValue, NormalizedError, PlaybookCallRequest, PlaybookCallResult, PlaybookCallStart, PlaybookPendingCall, PlaybookEffectLedger, PlaybookRepositoryDisposition, PlaybookRuntimeSnapshot, PlaybookSession, PlaybookState, PlaybookSuspendedCall, PlayerResult } from './runtime.js';
|
|
2
|
+
import type { CaptainResult, JsonValue, NormalizedError, PlaybookFailureCause, PlaybookCallRequest, PlaybookCallResult, PlaybookCallStart, PlaybookPendingCall, PlaybookEffectLedger, PlaybookRepositoryDisposition, PlaybookRuntimeSnapshot, PlaybookSession, PlaybookState, PlaybookSuspendedCall, PlayerResult } from './runtime.js';
|
|
3
3
|
export * from './xstate-playbook-runtime.js';
|
|
4
4
|
/**
|
|
5
5
|
* Immutable cancellation provenance for one runtime operation. The captured
|
|
@@ -27,6 +27,12 @@ export declare function snapshotJsonValue(value: unknown, path?: string): JsonVa
|
|
|
27
27
|
/** Validate session causality and detach its immutable identity from the host. */
|
|
28
28
|
export declare function snapshotPlaybookSession(session: PlaybookSession): PlaybookSession;
|
|
29
29
|
export declare function hiddenControlEnvelope(prompt: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* Attach one validated failure cause to an error object, returning it. A cause
|
|
32
|
+
* the closed validator rejects is dropped rather than published, and an error
|
|
33
|
+
* that refuses the property keeps its own cause-free identity (DR-063 §1).
|
|
34
|
+
*/
|
|
35
|
+
export declare function attachPlaybookFailureCause<E>(error: E, cause: unknown): E;
|
|
30
36
|
export declare function normalizeError(error: unknown): NormalizedError;
|
|
31
37
|
export interface PlaybookStateMetadata {
|
|
32
38
|
stateId: string;
|
|
@@ -74,6 +80,8 @@ export type PlaybookSemanticReconciliation = {
|
|
|
74
80
|
} | {
|
|
75
81
|
readonly status: 'unresolved';
|
|
76
82
|
readonly reason: PlaybookSemanticReconciliationReason;
|
|
83
|
+
/** DR-063 §1: why it stays unresolved, as a closed structured cause. */
|
|
84
|
+
readonly cause: PlaybookFailureCause;
|
|
77
85
|
readonly evidence: PlaybookRetainedSemanticEvidence;
|
|
78
86
|
};
|
|
79
87
|
/**
|
package/src/xstate-runtime.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
4
5
|
import { fromPromise, waitFor, } from 'xstate';
|
|
6
|
+
import { assertPlaybookFailureCause } from './runtime.js';
|
|
5
7
|
// DR-019: the generic linked-runtime factory and its strategy helpers live
|
|
6
8
|
// in the sibling module and are re-exported here so linked artifacts import
|
|
7
9
|
// one shared engine surface.
|
|
@@ -387,7 +389,74 @@ export function hiddenControlEnvelope(prompt) {
|
|
|
387
389
|
'Now return exactly one JSON object and nothing else.',
|
|
388
390
|
].join('\n\n');
|
|
389
391
|
}
|
|
392
|
+
/**
|
|
393
|
+
* DR-063 §2: the marker property the runtime writes on the error it marks as
|
|
394
|
+
* the FSM failure. It is non-enumerable, so decorating a host-owned error
|
|
395
|
+
* changes nothing a host can observe by enumeration or serialization.
|
|
396
|
+
*/
|
|
397
|
+
const PLAYBOOK_FAILURE_CAUSE_PROPERTY = 'playbookCause';
|
|
398
|
+
/**
|
|
399
|
+
* Attach one validated failure cause to an error object, returning it. A cause
|
|
400
|
+
* the closed validator rejects is dropped rather than published, and an error
|
|
401
|
+
* that refuses the property keeps its own cause-free identity (DR-063 §1).
|
|
402
|
+
*/
|
|
403
|
+
export function attachPlaybookFailureCause(error, cause) {
|
|
404
|
+
if (typeof error !== 'object' || error === null)
|
|
405
|
+
return error;
|
|
406
|
+
let validated;
|
|
407
|
+
try {
|
|
408
|
+
validated = assertPlaybookFailureCause(cause);
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
return error;
|
|
412
|
+
}
|
|
413
|
+
try {
|
|
414
|
+
Object.defineProperty(error, PLAYBOOK_FAILURE_CAUSE_PROPERTY, {
|
|
415
|
+
value: validated,
|
|
416
|
+
enumerable: false,
|
|
417
|
+
writable: true,
|
|
418
|
+
configurable: true,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
// A frozen or hostile error keeps no cause; the runtime's own slot still
|
|
423
|
+
// supplies one at the failed state.
|
|
424
|
+
}
|
|
425
|
+
return error;
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* DR-063 §2: a normalized error carries `cause` exactly when the underlying
|
|
429
|
+
* error carries a valid one. An `Error` carries it on the marker property the
|
|
430
|
+
* runtime writes; an already-normalized record carries it as `cause`, so the
|
|
431
|
+
* cause survives every re-normalization along the durable round trip.
|
|
432
|
+
*/
|
|
433
|
+
function playbookFailureCauseOf(error) {
|
|
434
|
+
if (typeof error !== 'object' || error === null)
|
|
435
|
+
return undefined;
|
|
436
|
+
let candidate;
|
|
437
|
+
try {
|
|
438
|
+
if (Object.hasOwn(error, PLAYBOOK_FAILURE_CAUSE_PROPERTY)) {
|
|
439
|
+
candidate = error[PLAYBOOK_FAILURE_CAUSE_PROPERTY];
|
|
440
|
+
}
|
|
441
|
+
else if (!(error instanceof Error) && Object.hasOwn(error, 'cause')) {
|
|
442
|
+
candidate = error.cause;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
catch {
|
|
446
|
+
return undefined;
|
|
447
|
+
}
|
|
448
|
+
if (candidate === undefined)
|
|
449
|
+
return undefined;
|
|
450
|
+
try {
|
|
451
|
+
return assertPlaybookFailureCause(candidate);
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
return undefined;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
390
457
|
export function normalizeError(error) {
|
|
458
|
+
const cause = playbookFailureCauseOf(error);
|
|
459
|
+
const withCause = (normalized) => cause === undefined ? normalized : { ...normalized, cause };
|
|
391
460
|
if (error instanceof Error) {
|
|
392
461
|
let name = 'Error';
|
|
393
462
|
let message = 'Unknown error';
|
|
@@ -414,11 +483,11 @@ export function normalizeError(error) {
|
|
|
414
483
|
catch {
|
|
415
484
|
// A stack is optional at the public boundary.
|
|
416
485
|
}
|
|
417
|
-
return {
|
|
486
|
+
return withCause({
|
|
418
487
|
name,
|
|
419
488
|
message,
|
|
420
489
|
...(stack ? { stack } : {}),
|
|
421
|
-
};
|
|
490
|
+
});
|
|
422
491
|
}
|
|
423
492
|
if (typeof error === 'string') {
|
|
424
493
|
return { name: 'Error', message: error };
|
|
@@ -426,22 +495,22 @@ export function normalizeError(error) {
|
|
|
426
495
|
try {
|
|
427
496
|
assertJsonSafe(error);
|
|
428
497
|
if (isRecord(error) && typeof error.message === 'string') {
|
|
429
|
-
return {
|
|
498
|
+
return withCause({
|
|
430
499
|
name: typeof error.name === 'string' && error.name.length > 0
|
|
431
500
|
? error.name
|
|
432
501
|
: 'Error',
|
|
433
502
|
message: error.message,
|
|
434
503
|
...(typeof error.stack === 'string' ? { stack: error.stack } : {}),
|
|
435
|
-
};
|
|
504
|
+
});
|
|
436
505
|
}
|
|
437
|
-
return { name: 'Error', message: JSON.stringify(error) };
|
|
506
|
+
return withCause({ name: 'Error', message: JSON.stringify(error) });
|
|
438
507
|
}
|
|
439
508
|
catch {
|
|
440
509
|
try {
|
|
441
|
-
return { name: 'Error', message: String(error) };
|
|
510
|
+
return withCause({ name: 'Error', message: String(error) });
|
|
442
511
|
}
|
|
443
512
|
catch {
|
|
444
|
-
return { name: 'Error', message: 'Unknown error' };
|
|
513
|
+
return withCause({ name: 'Error', message: 'Unknown error' });
|
|
445
514
|
}
|
|
446
515
|
}
|
|
447
516
|
}
|
|
@@ -629,9 +698,56 @@ function projectionsEqual(left, right) {
|
|
|
629
698
|
return (left.projectionDigest === right.projectionDigest &&
|
|
630
699
|
projectionText(left.projection) === projectionText(right.projection));
|
|
631
700
|
}
|
|
632
|
-
function
|
|
633
|
-
return
|
|
634
|
-
JSON.stringify(
|
|
701
|
+
function projectionEntriesEqual(left, right, path) {
|
|
702
|
+
return (own(right.projection, path) &&
|
|
703
|
+
JSON.stringify(left.projection[path]) ===
|
|
704
|
+
JSON.stringify(right.projection[path]));
|
|
705
|
+
}
|
|
706
|
+
const EFFECT_PRE_EXISTING_LISTS = ['absorbed', 'altered', 'lost'];
|
|
707
|
+
const EMPTY_PRE_EXISTING = Object.freeze({
|
|
708
|
+
absorbed: Object.freeze([]),
|
|
709
|
+
altered: Object.freeze([]),
|
|
710
|
+
lost: Object.freeze([]),
|
|
711
|
+
});
|
|
712
|
+
/**
|
|
713
|
+
* The pre-existing accounting a physical or logical receipt records
|
|
714
|
+
* (DR-062 §3): sorted unique baseline paths, pairwise disjoint, present
|
|
715
|
+
* exactly when one list is nonempty.
|
|
716
|
+
*/
|
|
717
|
+
function effectPreExisting(value, path, baseline) {
|
|
718
|
+
if (!isRecord(value))
|
|
719
|
+
throw new TypeError(`${path} must be an object`);
|
|
720
|
+
rejectUnknownKeys(value, EFFECT_PRE_EXISTING_LISTS, path);
|
|
721
|
+
const claimed = new Set();
|
|
722
|
+
const lists = EFFECT_PRE_EXISTING_LISTS.map((name) => {
|
|
723
|
+
const entries = value[name];
|
|
724
|
+
if (!Array.isArray(entries)) {
|
|
725
|
+
throw new TypeError(`${path}.${name} must be an array of baseline paths`);
|
|
726
|
+
}
|
|
727
|
+
let previous;
|
|
728
|
+
for (const entry of entries) {
|
|
729
|
+
if (typeof entry !== 'string' || entry.length === 0) {
|
|
730
|
+
throw new TypeError(`${path}.${name} must hold nonempty baseline paths`);
|
|
731
|
+
}
|
|
732
|
+
const member = entry;
|
|
733
|
+
if (previous !== undefined && member <= previous) {
|
|
734
|
+
throw new TypeError(`${path}.${name} must be sorted and unique`);
|
|
735
|
+
}
|
|
736
|
+
previous = member;
|
|
737
|
+
if (!own(baseline.projection, member)) {
|
|
738
|
+
throw new TypeError(`${path}.${name} names a path outside the baseline projection`);
|
|
739
|
+
}
|
|
740
|
+
if (claimed.has(member)) {
|
|
741
|
+
throw new TypeError(`${path} lists ${JSON.stringify(member)} twice`);
|
|
742
|
+
}
|
|
743
|
+
claimed.add(member);
|
|
744
|
+
}
|
|
745
|
+
return [name, entries];
|
|
746
|
+
});
|
|
747
|
+
if (claimed.size === 0) {
|
|
748
|
+
throw new TypeError(`${path} must name at least one baseline path`);
|
|
749
|
+
}
|
|
750
|
+
return Object.fromEntries(lists);
|
|
635
751
|
}
|
|
636
752
|
function effectObservation(value, path) {
|
|
637
753
|
if (!isRecord(value))
|
|
@@ -672,7 +788,7 @@ function assertObservationIdentity(observation, identity, path) {
|
|
|
672
788
|
function effectReceipt(value, path, expectedBaseline, expectedAfter, matchExpectedAfter = false, dispositions) {
|
|
673
789
|
if (!isRecord(value))
|
|
674
790
|
throw new TypeError(`${path} must be an object`);
|
|
675
|
-
rejectUnknownKeys(value, ['classification', 'baseline', 'after', 'commitOid'], path);
|
|
791
|
+
rejectUnknownKeys(value, ['classification', 'baseline', 'after', 'commitOid', 'preExisting'], path);
|
|
676
792
|
if (typeof value.classification !== 'string' ||
|
|
677
793
|
!EFFECT_RECEIPT_CLASSIFICATIONS.has(value.classification)) {
|
|
678
794
|
throw new TypeError(`${path}.classification is not supported`);
|
|
@@ -718,18 +834,38 @@ function effectReceipt(value, path, expectedBaseline, expectedAfter, matchExpect
|
|
|
718
834
|
!jsonValuesEqual(baseline, after)) {
|
|
719
835
|
throw new TypeError(`${path} classified unchanged observations that differ`);
|
|
720
836
|
}
|
|
837
|
+
// DR-062 §3: a receipt records the fate of every baseline entry, so the
|
|
838
|
+
// accounting and the two observations must agree exactly.
|
|
839
|
+
if (own(value, 'preExisting') &&
|
|
840
|
+
classification !== 'one-descendant-commit' &&
|
|
841
|
+
classification !== 'worktree-only-change' &&
|
|
842
|
+
classification !== 'observation-ambiguous') {
|
|
843
|
+
throw new TypeError(`${path}.preExisting is not permitted for ${classification}`);
|
|
844
|
+
}
|
|
845
|
+
const preExisting = own(value, 'preExisting')
|
|
846
|
+
? effectPreExisting(value.preExisting, `${path}.preExisting`, baseline)
|
|
847
|
+
: EMPTY_PRE_EXISTING;
|
|
848
|
+
const consumed = new Set([...preExisting.absorbed, ...preExisting.altered]);
|
|
721
849
|
if (classification === 'one-descendant-commit' &&
|
|
722
850
|
after !== undefined &&
|
|
723
851
|
(baseline.head === after.head ||
|
|
724
|
-
|
|
725
|
-
|
|
852
|
+
preExisting.lost.length > 0 ||
|
|
853
|
+
!Object.keys(after.projection).every((member) => own(baseline.projection, member) &&
|
|
854
|
+
projectionEntriesEqual(baseline, after, member)) ||
|
|
855
|
+
!Object.keys(baseline.projection).every((member) => own(after.projection, member) !== consumed.has(member)) ||
|
|
856
|
+
(consumed.size === 0) !== projectionsEqual(baseline, after))) {
|
|
857
|
+
throw new TypeError(`${path} one-descendant-commit must change HEAD and account for its complete baseline projection`);
|
|
726
858
|
}
|
|
727
859
|
if (classification === 'worktree-only-change' &&
|
|
728
860
|
after !== undefined &&
|
|
729
861
|
(baseline.head !== after.head ||
|
|
730
862
|
projectionsEqual(baseline, after) ||
|
|
731
|
-
|
|
732
|
-
|
|
863
|
+
preExisting.absorbed.length > 0 ||
|
|
864
|
+
preExisting.lost.length > 0 ||
|
|
865
|
+
!Object.keys(baseline.projection).every((member) => own(after.projection, member) &&
|
|
866
|
+
projectionEntriesEqual(baseline, after, member) !==
|
|
867
|
+
consumed.has(member)))) {
|
|
868
|
+
throw new TypeError(`${path} worktree-only-change must preserve HEAD, change the projection, and account for every altered baseline entry`);
|
|
733
869
|
}
|
|
734
870
|
if ((classification === 'multiple-commits' ||
|
|
735
871
|
classification === 'rewritten-or-non-descendant') &&
|
|
@@ -820,8 +956,137 @@ function retainedSemanticEvidence(candidate, finalText) {
|
|
|
820
956
|
...(typeof finalText === 'string' ? { finalText } : {}),
|
|
821
957
|
});
|
|
822
958
|
}
|
|
823
|
-
|
|
824
|
-
|
|
959
|
+
// ---------------------------------------------------------------------------
|
|
960
|
+
// DR-063 §1: the evidence builder for a repository-decided failure. Every list
|
|
961
|
+
// is sorted, unique, and bounded; `truncated` carries the count the bound
|
|
962
|
+
// omitted. Nothing but repository paths, dispositions, classifications, and
|
|
963
|
+
// revision identities enters a cause.
|
|
964
|
+
// ---------------------------------------------------------------------------
|
|
965
|
+
const FAILURE_PATH_BOUND = 32;
|
|
966
|
+
function boundedFailurePaths(lists) {
|
|
967
|
+
const bounded = {};
|
|
968
|
+
let omitted = 0;
|
|
969
|
+
for (const [member, paths] of Object.entries(lists)) {
|
|
970
|
+
bounded[member] = paths.slice(0, FAILURE_PATH_BOUND);
|
|
971
|
+
omitted += Math.max(0, paths.length - FAILURE_PATH_BOUND);
|
|
972
|
+
}
|
|
973
|
+
if (omitted > 0)
|
|
974
|
+
bounded.truncated = omitted;
|
|
975
|
+
return bounded;
|
|
976
|
+
}
|
|
977
|
+
function sortedUniquePaths(paths) {
|
|
978
|
+
return [...new Set(paths)].sort();
|
|
979
|
+
}
|
|
980
|
+
/** After-projection entries the baseline did not hold, or holds differently. */
|
|
981
|
+
function newOrChangedProjectionPaths(baseline, after) {
|
|
982
|
+
return sortedUniquePaths(Object.keys(after.projection).filter((path) => !Object.hasOwn(baseline.projection, path) ||
|
|
983
|
+
!isDeepStrictEqual(baseline.projection[path], after.projection[path])));
|
|
984
|
+
}
|
|
985
|
+
/** Entries that differ or are missing on either side of the two projections. */
|
|
986
|
+
function divergentProjectionPaths(baseline, after) {
|
|
987
|
+
return sortedUniquePaths([
|
|
988
|
+
...Object.keys(baseline.projection),
|
|
989
|
+
...Object.keys(after.projection),
|
|
990
|
+
].filter((path) => !Object.hasOwn(baseline.projection, path) ||
|
|
991
|
+
!Object.hasOwn(after.projection, path) ||
|
|
992
|
+
!isDeepStrictEqual(baseline.projection[path], after.projection[path])));
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* DR-063 §1: the cause of an unresolved repository-disposition mismatch, read
|
|
996
|
+
* from the disposition the accepted arm required and the receipt the host
|
|
997
|
+
* proved. The receipt states the failure; nothing here infers one.
|
|
998
|
+
*/
|
|
999
|
+
function playbookRepositoryFailureCause(required, receipt) {
|
|
1000
|
+
const observed = receipt.classification;
|
|
1001
|
+
const baselineHead = receipt.baseline.head;
|
|
1002
|
+
const after = receipt.after;
|
|
1003
|
+
const common = { required, observed, baselineHead };
|
|
1004
|
+
const ambiguous = () => assertPlaybookFailureCause({
|
|
1005
|
+
code: 'attribution-ambiguous',
|
|
1006
|
+
evidence: {
|
|
1007
|
+
...common,
|
|
1008
|
+
...(after === undefined ? {} : { afterHead: after.head }),
|
|
1009
|
+
},
|
|
1010
|
+
});
|
|
1011
|
+
const lost = receipt.preExisting?.lost ?? [];
|
|
1012
|
+
if (lost.length > 0) {
|
|
1013
|
+
return assertPlaybookFailureCause({
|
|
1014
|
+
code: 'pre-existing-lost',
|
|
1015
|
+
evidence: {
|
|
1016
|
+
...common,
|
|
1017
|
+
...(after === undefined ? {} : { afterHead: after.head }),
|
|
1018
|
+
paths: boundedFailurePaths({ lost: sortedUniquePaths(lost) }),
|
|
1019
|
+
},
|
|
1020
|
+
});
|
|
1021
|
+
}
|
|
1022
|
+
if (observed === 'observation-ambiguous' && after === undefined) {
|
|
1023
|
+
return assertPlaybookFailureCause({
|
|
1024
|
+
code: 'observation-unstable',
|
|
1025
|
+
evidence: common,
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
if (after === undefined)
|
|
1029
|
+
return ambiguous();
|
|
1030
|
+
const afterHead = after.head;
|
|
1031
|
+
if (observed === 'multiple-commits') {
|
|
1032
|
+
return assertPlaybookFailureCause({
|
|
1033
|
+
code: 'commits-more-than-one',
|
|
1034
|
+
evidence: { ...common, afterHead },
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
if (observed === 'rewritten-or-non-descendant') {
|
|
1038
|
+
return assertPlaybookFailureCause({
|
|
1039
|
+
code: 'history-rewritten',
|
|
1040
|
+
evidence: { ...common, afterHead },
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
if (observed === 'concurrent-or-foreign-change') {
|
|
1044
|
+
return assertPlaybookFailureCause({
|
|
1045
|
+
code: 'foreign-change',
|
|
1046
|
+
evidence: {
|
|
1047
|
+
...common,
|
|
1048
|
+
afterHead,
|
|
1049
|
+
paths: boundedFailurePaths({
|
|
1050
|
+
changed: divergentProjectionPaths(receipt.baseline, after),
|
|
1051
|
+
}),
|
|
1052
|
+
},
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
const uncommitted = newOrChangedProjectionPaths(receipt.baseline, after);
|
|
1056
|
+
const altered = sortedUniquePaths(receipt.preExisting?.altered ?? []);
|
|
1057
|
+
if (observed === 'observation-ambiguous' &&
|
|
1058
|
+
afterHead !== baselineHead &&
|
|
1059
|
+
(uncommitted.length > 0 || altered.length > 0)) {
|
|
1060
|
+
return assertPlaybookFailureCause({
|
|
1061
|
+
code: 'commit-residual',
|
|
1062
|
+
evidence: {
|
|
1063
|
+
...common,
|
|
1064
|
+
afterHead,
|
|
1065
|
+
...(receipt.commitOid === undefined
|
|
1066
|
+
? {}
|
|
1067
|
+
: { commitOid: receipt.commitOid }),
|
|
1068
|
+
paths: boundedFailurePaths({ uncommitted, altered }),
|
|
1069
|
+
},
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
if (required === 'one-descendant-commit' &&
|
|
1073
|
+
(observed === 'unchanged' || observed === 'worktree-only-change')) {
|
|
1074
|
+
return assertPlaybookFailureCause({
|
|
1075
|
+
code: 'commit-missing',
|
|
1076
|
+
evidence: {
|
|
1077
|
+
...common,
|
|
1078
|
+
afterHead,
|
|
1079
|
+
paths: boundedFailurePaths({ uncommitted }),
|
|
1080
|
+
},
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
return ambiguous();
|
|
1084
|
+
}
|
|
1085
|
+
function unresolvedSemanticEvidence(reason, evidence, cause = assertPlaybookFailureCause({
|
|
1086
|
+
code: 'runtime-defect',
|
|
1087
|
+
evidence: { reason },
|
|
1088
|
+
})) {
|
|
1089
|
+
return Object.freeze({ status: 'unresolved', reason, cause, evidence });
|
|
825
1090
|
}
|
|
826
1091
|
/**
|
|
827
1092
|
* Reconcile one exact semantic candidate with host-owned effect evidence.
|
|
@@ -860,7 +1125,7 @@ export function reconcilePlaybookSemanticEvidence(input) {
|
|
|
860
1125
|
receipt.after !== undefined &&
|
|
861
1126
|
receipt.after.head === receipt.baseline.head);
|
|
862
1127
|
if (!dispositionMatches) {
|
|
863
|
-
return unresolvedSemanticEvidence('repository-disposition-mismatch', evidence);
|
|
1128
|
+
return unresolvedSemanticEvidence('repository-disposition-mismatch', evidence, playbookRepositoryFailureCause(disposition, receipt));
|
|
864
1129
|
}
|
|
865
1130
|
let runtimeFields = Object.freeze({});
|
|
866
1131
|
if (input.runtimeFields !== undefined) {
|
|
@@ -1630,6 +1895,17 @@ export class NestedPlaybookCallError extends Error {
|
|
|
1630
1895
|
if (normalized?.stack)
|
|
1631
1896
|
this.stack = normalized.stack;
|
|
1632
1897
|
this.result = result;
|
|
1898
|
+
// DR-063 §2: the child's failure reaching its parent is `child-failed`,
|
|
1899
|
+
// carrying the child's own cause when its normalized error published one.
|
|
1900
|
+
attachPlaybookFailureCause(this, {
|
|
1901
|
+
code: 'child-failed',
|
|
1902
|
+
evidence: {
|
|
1903
|
+
playbookId: result.playbookId,
|
|
1904
|
+
...(normalized?.cause === undefined
|
|
1905
|
+
? {}
|
|
1906
|
+
: { cause: normalized.cause }),
|
|
1907
|
+
},
|
|
1908
|
+
});
|
|
1633
1909
|
}
|
|
1634
1910
|
}
|
|
1635
1911
|
function validateState(state, path) {
|
|
@@ -1720,7 +1996,7 @@ function validateNormalizedError(error, path) {
|
|
|
1720
1996
|
if (!isRecord(error)) {
|
|
1721
1997
|
throw new TypeError(`${path} must be a normalized error`);
|
|
1722
1998
|
}
|
|
1723
|
-
rejectUnknownKeys(error, ['name', 'message', 'stack'], path);
|
|
1999
|
+
rejectUnknownKeys(error, ['name', 'message', 'stack', 'cause'], path);
|
|
1724
2000
|
requireNonEmptyString(error.name, `${path}.name`);
|
|
1725
2001
|
if (typeof error.message !== 'string') {
|
|
1726
2002
|
throw new TypeError(`${path}.message must be a string`);
|
|
@@ -1728,6 +2004,10 @@ function validateNormalizedError(error, path) {
|
|
|
1728
2004
|
if (error.stack !== undefined && typeof error.stack !== 'string') {
|
|
1729
2005
|
throw new TypeError(`${path}.stack must be a string`);
|
|
1730
2006
|
}
|
|
2007
|
+
// DR-063 §2: a normalized error may carry its structured cause, and an
|
|
2008
|
+
// unrecognized shape is not a cause.
|
|
2009
|
+
if (error.cause !== undefined)
|
|
2010
|
+
assertPlaybookFailureCause(error.cause);
|
|
1731
2011
|
}
|
|
1732
2012
|
// DR-048: the completed child's compiled terminal record. It is runtime-owned
|
|
1733
2013
|
// data read from the child's artifact, so a malformed one is a control-plane
|