@sublang/playbook 17.7.0 → 17.8.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/repository-effects.js +30 -12
- package/reference/sdlc/code.playbook/host-capabilities.d.ts +2 -0
- package/reference/sdlc/code.playbook/playbook-captain.js +162 -12
- package/reference/sdlc/code.playbook/playbook-captain.ts +217 -11
- package/slc/link.md +29 -6
- package/slc/text2gears.md +5 -3
- package/src/runtime.d.ts +6 -0
- package/src/runtime.ts +6 -0
- package/src/xstate-playbook-runtime.d.ts +20 -3
- package/src/xstate-playbook-runtime.js +267 -18
- package/src/xstate-playbook-runtime.ts +375 -21
- package/src/xstate-runtime.d.ts +23 -3
- package/src/xstate-runtime.js +97 -13
- package/src/xstate-runtime.ts +159 -19
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sublang/playbook",
|
|
3
|
-
"version": "17.
|
|
3
|
+
"version": "17.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Composable XState v5 playbook runtime with compiled Captain, CODE, REVIEW, DECIDE, DEV, BRANCH, PR, and INSPECT workflows driven by GEARS specs.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -976,27 +976,43 @@ async function isAncestor(worktree, baselineHead, afterHead) {
|
|
|
976
976
|
}
|
|
977
977
|
}
|
|
978
978
|
|
|
979
|
-
|
|
980
|
-
|
|
979
|
+
// DR-088 §1: the commits reachable from the after HEAD and not from the
|
|
980
|
+
// baseline HEAD — every reachable commit for the null HEAD — oldest first, so
|
|
981
|
+
// the last is the after HEAD itself.
|
|
982
|
+
async function newCommits(worktree, baselineHead, afterHead) {
|
|
983
|
+
const raw = await runGitText(worktree, [
|
|
981
984
|
'rev-list',
|
|
982
|
-
'--
|
|
985
|
+
'--topo-order',
|
|
986
|
+
'--reverse',
|
|
983
987
|
baselineHead === NULL_GIT_OID ? afterHead : `${baselineHead}..${afterHead}`,
|
|
984
988
|
]);
|
|
985
|
-
const
|
|
986
|
-
if (
|
|
989
|
+
const commits = raw.length === 0 ? [] : raw.split('\n');
|
|
990
|
+
if (
|
|
991
|
+
commits.some((oid) => !/^[0-9a-f]{40}(?:[0-9a-f]{24})?$/.test(oid)) ||
|
|
992
|
+
new Set(commits).size !== commits.length ||
|
|
993
|
+
(commits.length > 0 && commits.at(-1) !== afterHead)
|
|
994
|
+
) {
|
|
987
995
|
throw new RepositoryObservationAmbiguousError(
|
|
988
|
-
'Git returned an invalid
|
|
996
|
+
'Git returned an invalid list of new commits',
|
|
989
997
|
);
|
|
990
998
|
}
|
|
991
|
-
return
|
|
999
|
+
return commits;
|
|
992
1000
|
}
|
|
993
1001
|
|
|
994
|
-
function receipt(
|
|
1002
|
+
function receipt(
|
|
1003
|
+
classification,
|
|
1004
|
+
baseline,
|
|
1005
|
+
after,
|
|
1006
|
+
commitOid,
|
|
1007
|
+
preExisting,
|
|
1008
|
+
commits,
|
|
1009
|
+
) {
|
|
995
1010
|
return deepFreeze({
|
|
996
1011
|
classification,
|
|
997
1012
|
baseline,
|
|
998
1013
|
...(after === undefined ? {} : { after }),
|
|
999
1014
|
...(commitOid === undefined ? {} : { commitOid }),
|
|
1015
|
+
...(commits === undefined ? {} : { commits }),
|
|
1000
1016
|
...(preExisting === undefined ? {} : { preExisting }),
|
|
1001
1017
|
});
|
|
1002
1018
|
}
|
|
@@ -1049,12 +1065,13 @@ export async function classifyRepositoryReceipt(
|
|
|
1049
1065
|
) {
|
|
1050
1066
|
return receipt('rewritten-or-non-descendant', baseline, after);
|
|
1051
1067
|
}
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1068
|
+
// DR-088 §1: HEAD advanced by one or more new commits; a receipt of more
|
|
1069
|
+
// than one lists every one, oldest first.
|
|
1070
|
+
const commits = await newCommits(after.worktree, baseline.head, after.head);
|
|
1071
|
+
if (commits.length === 0) {
|
|
1055
1072
|
return receipt('rewritten-or-non-descendant', baseline, after);
|
|
1056
1073
|
}
|
|
1057
|
-
// DR-062 §2: the
|
|
1074
|
+
// DR-062 §2: the advance proves itself when every baseline entry is
|
|
1058
1075
|
// preserved, absorbed, or altered and carried by it, and the after
|
|
1059
1076
|
// projection holds nothing else.
|
|
1060
1077
|
const fates = await baselineEntryFates(baseline, after);
|
|
@@ -1071,6 +1088,7 @@ export async function classifyRepositoryReceipt(
|
|
|
1071
1088
|
after,
|
|
1072
1089
|
unattributable ? undefined : after.head,
|
|
1073
1090
|
preExistingFromFates(fates),
|
|
1091
|
+
unattributable || commits.length === 1 ? undefined : commits,
|
|
1074
1092
|
);
|
|
1075
1093
|
}
|
|
1076
1094
|
|
|
@@ -42,6 +42,8 @@ export interface PlaybookRepositoryReceipt {
|
|
|
42
42
|
readonly baseline: PlaybookRepositoryObservation;
|
|
43
43
|
readonly after?: PlaybookRepositoryObservation;
|
|
44
44
|
readonly commitOid?: string;
|
|
45
|
+
/** Every new commit, oldest first, where HEAD advanced by more than one (DR-088). */
|
|
46
|
+
readonly commits?: readonly string[];
|
|
45
47
|
readonly preExisting?: PlaybookRepositoryPreExistingChanges;
|
|
46
48
|
}
|
|
47
49
|
export interface RepositoryReceiptOptions {
|
|
@@ -621,6 +621,112 @@ function carriedPreExistingFacts(carried) {
|
|
|
621
621
|
return `The commit ${commitOid} carries changes that were uncommitted before the step: ${lists.join('; ')}.`;
|
|
622
622
|
});
|
|
623
623
|
}
|
|
624
|
+
function commitsBeforeFailure(ledger, boundaryPrefixCount, unresolvedEffects) {
|
|
625
|
+
// An entry names a commit by its proven OID, or — for a receipt listing
|
|
626
|
+
// several commits, which projects as `multiple-commits` (DR-088 §3) — by
|
|
627
|
+
// its after HEAD, the last of them.
|
|
628
|
+
const named = new Set(unresolvedEffects.flatMap(({ classification, afterHead, commitOid }) => commitOid !== undefined
|
|
629
|
+
? [commitOid]
|
|
630
|
+
: classification === 'multiple-commits' && afterHead !== undefined
|
|
631
|
+
? [afterHead]
|
|
632
|
+
: []));
|
|
633
|
+
const commits = [];
|
|
634
|
+
for (const boundary of ledger.boundaries) {
|
|
635
|
+
if (boundary.sequence <= boundaryPrefixCount)
|
|
636
|
+
continue;
|
|
637
|
+
const receipt = boundary.physicalReceipt;
|
|
638
|
+
if (receipt?.classification !== 'one-descendant-commit' ||
|
|
639
|
+
receipt.commitOid === undefined ||
|
|
640
|
+
named.has(receipt.commitOid)) {
|
|
641
|
+
continue;
|
|
642
|
+
}
|
|
643
|
+
named.add(receipt.commitOid);
|
|
644
|
+
commits.push({
|
|
645
|
+
baselineHead: receipt.baseline.head,
|
|
646
|
+
commitOid: receipt.commitOid,
|
|
647
|
+
...(receipt.commits === undefined ? {} : { commits: receipt.commits }),
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
return commits;
|
|
651
|
+
}
|
|
652
|
+
/** What one step committed, with every commit where it made several (DR-088). */
|
|
653
|
+
function commitBeforeFailureClause({ baselineHead, commitOid, commits, }) {
|
|
654
|
+
const made = commits === undefined
|
|
655
|
+
? 'made one new commit'
|
|
656
|
+
: `made ${commits.length} new commits, oldest first: ${boundedPathList(commits)}`;
|
|
657
|
+
return `${made}; HEAD moved from ${baselineHead} to the proven commit ${commitOid}.`;
|
|
658
|
+
}
|
|
659
|
+
function commitBeforeFailureFact(commit) {
|
|
660
|
+
return `Before the failure, a step ${commitBeforeFailureClause(commit)}`;
|
|
661
|
+
}
|
|
662
|
+
function commitsBeforeFailureReport(commits) {
|
|
663
|
+
if (commits.length === 0)
|
|
664
|
+
return undefined;
|
|
665
|
+
return [
|
|
666
|
+
'Repository changes made before the failure:',
|
|
667
|
+
...commits.map((commit) => `- A step ${commitBeforeFailureClause(commit)}`),
|
|
668
|
+
'The failure does not undo these commits.',
|
|
669
|
+
].join('\n');
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* DR-088 §2: whether a final text names one commit as a word, by its full OID
|
|
673
|
+
* or by an abbreviation of at least seven leading hex digits — the
|
|
674
|
+
* reconciler's own designation rule, read here to say what it accepted.
|
|
675
|
+
*/
|
|
676
|
+
function finalTextNamesCommit(finalText, oid) {
|
|
677
|
+
for (const [word] of finalText.matchAll(/(?<![0-9A-Za-z_])[0-9A-Fa-f]{7,64}(?![0-9A-Za-z_])/g)) {
|
|
678
|
+
if (oid.startsWith(word.toLowerCase()))
|
|
679
|
+
return true;
|
|
680
|
+
}
|
|
681
|
+
return false;
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* DR-088 §3: what the steps completed after `boundaryPrefixCount` committed,
|
|
685
|
+
* one statement each, in boundary sequence order: every new commit where a
|
|
686
|
+
* receipt proves more than one, and the commit already at HEAD where a step
|
|
687
|
+
* whose outcomes all commit or ask named it over an `unchanged` receipt. A
|
|
688
|
+
* step in a deferred chain reads the chain's cumulative receipt at its last
|
|
689
|
+
* boundary.
|
|
690
|
+
*/
|
|
691
|
+
function stepCommitStatements(ledger, boundaryPrefixCount) {
|
|
692
|
+
const statements = [];
|
|
693
|
+
for (const boundary of ledger.boundaries) {
|
|
694
|
+
if (boundary.sequence <= boundaryPrefixCount)
|
|
695
|
+
continue;
|
|
696
|
+
let receipt = boundary.physicalReceipt;
|
|
697
|
+
if (boundary.logicalOperationId !== undefined) {
|
|
698
|
+
const operation = ledger.logicalOperations.find(({ operationId }) => operationId === boundary.logicalOperationId);
|
|
699
|
+
if (operation?.boundaryIds.at(-1) !== boundary.boundaryId)
|
|
700
|
+
continue;
|
|
701
|
+
receipt = operation.logicalReceipt;
|
|
702
|
+
}
|
|
703
|
+
if (receipt === undefined)
|
|
704
|
+
continue;
|
|
705
|
+
if (receipt.classification === 'one-descendant-commit' &&
|
|
706
|
+
receipt.commits !== undefined) {
|
|
707
|
+
statements.push(`The step made ${receipt.commits.length} new commits, oldest first: ${boundedPathList(receipt.commits)}.`);
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
const candidate = boundary.semanticCandidate;
|
|
711
|
+
const guard = candidate !== null &&
|
|
712
|
+
typeof candidate === 'object' &&
|
|
713
|
+
!Array.isArray(candidate)
|
|
714
|
+
? candidate.guard
|
|
715
|
+
: undefined;
|
|
716
|
+
const head = receipt.after?.head;
|
|
717
|
+
if (receipt.classification === 'unchanged' &&
|
|
718
|
+
head !== undefined &&
|
|
719
|
+
!/^0+$/.test(head) &&
|
|
720
|
+
boundary.dispositions.includes('one-descendant-commit') &&
|
|
721
|
+
!boundary.dispositions.includes('unchanged') &&
|
|
722
|
+
typeof guard === 'string' &&
|
|
723
|
+
guard !== BOSS_QUESTION_OUTCOME &&
|
|
724
|
+
finalTextNamesCommit(boundary.finalText?.trim() ?? '', head)) {
|
|
725
|
+
statements.push(`The step made no new commit; it named ${head}, already at HEAD, as its work.`);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
return statements;
|
|
729
|
+
}
|
|
624
730
|
// DR-063 §4: the Boss-visible failure report. Every host sees the same
|
|
625
731
|
// sentences because the shell composes them from the structured cause instead
|
|
626
732
|
// of asking a model to describe facts it may drop. The report exposes no file
|
|
@@ -2402,6 +2508,15 @@ async function buildEnablements(options, loadModule, hostCapabilities) {
|
|
|
2402
2508
|
const unresolvedEffectFromReceipt = (receipt, baselineHead = receipt.baseline.head) => {
|
|
2403
2509
|
if (receipt.classification === 'unchanged')
|
|
2404
2510
|
return undefined;
|
|
2511
|
+
// DR-088 §3: a receipt listing more than one new commit reads as
|
|
2512
|
+
// `multiple-commits`, whose entry names more than one commit and no OID.
|
|
2513
|
+
if (receipt.commits !== undefined) {
|
|
2514
|
+
return {
|
|
2515
|
+
classification: 'multiple-commits',
|
|
2516
|
+
baselineHead,
|
|
2517
|
+
...(receipt.after === undefined ? {} : { afterHead: receipt.after.head }),
|
|
2518
|
+
};
|
|
2519
|
+
}
|
|
2405
2520
|
return {
|
|
2406
2521
|
classification: receipt.classification,
|
|
2407
2522
|
baselineHead,
|
|
@@ -2478,12 +2593,18 @@ const cumulativeOpenLogicalEffect = (operation, boundaries) => {
|
|
|
2478
2593
|
}
|
|
2479
2594
|
if (receipt.classification === 'one-descendant-commit' &&
|
|
2480
2595
|
(sameProjection || receiptAccountsFromOriginal)) {
|
|
2481
|
-
return
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2596
|
+
return receipt.commits === undefined
|
|
2597
|
+
? {
|
|
2598
|
+
classification: 'one-descendant-commit',
|
|
2599
|
+
baselineHead: original.head,
|
|
2600
|
+
afterHead: after.head,
|
|
2601
|
+
commitOid: after.head,
|
|
2602
|
+
}
|
|
2603
|
+
: {
|
|
2604
|
+
classification: 'multiple-commits',
|
|
2605
|
+
baselineHead: original.head,
|
|
2606
|
+
afterHead: after.head,
|
|
2607
|
+
};
|
|
2487
2608
|
}
|
|
2488
2609
|
return {
|
|
2489
2610
|
classification: 'observation-ambiguous',
|
|
@@ -2792,21 +2913,30 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
2792
2913
|
if (turn.carriedEffectPrefix !== undefined)
|
|
2793
2914
|
return;
|
|
2794
2915
|
let carried;
|
|
2916
|
+
let commitStatements;
|
|
2795
2917
|
try {
|
|
2796
2918
|
const ledger = assertPlaybookEffectLedger(currentEffectLedger());
|
|
2797
2919
|
carried = carriedPreExistingChanges(ledger, presentedEffectPrefix);
|
|
2920
|
+
commitStatements = stepCommitStatements(ledger, presentedEffectPrefix);
|
|
2798
2921
|
turn.carriedEffectPrefix = ledger.boundaries.length;
|
|
2799
2922
|
}
|
|
2800
2923
|
catch {
|
|
2801
2924
|
return;
|
|
2802
2925
|
}
|
|
2803
2926
|
const report = carriedPreExistingReport(carried);
|
|
2804
|
-
if (report
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2927
|
+
if (report !== undefined) {
|
|
2928
|
+
const facts = carriedPreExistingFacts(carried);
|
|
2929
|
+
appendMandatoryPresentationSuffix(turn, report, facts);
|
|
2930
|
+
for (const fact of facts) {
|
|
2931
|
+
joinSettlementFact(turn, fact);
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
// DR-088 §3: the commits each step took, read from the same boundaries.
|
|
2935
|
+
if (commitStatements.length > 0) {
|
|
2936
|
+
appendMandatoryPresentationSuffix(turn, commitStatements.join('\n'), commitStatements);
|
|
2937
|
+
for (const fact of commitStatements) {
|
|
2938
|
+
joinSettlementFact(turn, fact);
|
|
2939
|
+
}
|
|
2810
2940
|
}
|
|
2811
2941
|
};
|
|
2812
2942
|
/**
|
|
@@ -6203,6 +6333,26 @@ export function createPlaybookCaptainShell(options, deps = {}) {
|
|
|
6203
6333
|
controls: failureControlLines(view, fenced),
|
|
6204
6334
|
};
|
|
6205
6335
|
joinSettlementFact(turn, fact, statement?.fact, speak);
|
|
6336
|
+
reportCommitsBeforeFailure(turn, unresolvedEffects);
|
|
6337
|
+
};
|
|
6338
|
+
// DR-089: the commits the turn's receipts proved before its leaf parked,
|
|
6339
|
+
// stated beside the failure report and joined to the settlement facts the
|
|
6340
|
+
// closing-reply prompt reads.
|
|
6341
|
+
const reportCommitsBeforeFailure = (turn, unresolvedEffects) => {
|
|
6342
|
+
let commits;
|
|
6343
|
+
try {
|
|
6344
|
+
commits = commitsBeforeFailure(assertPlaybookEffectLedger(currentEffectLedger()), presentedEffectPrefix, unresolvedEffects);
|
|
6345
|
+
}
|
|
6346
|
+
catch {
|
|
6347
|
+
return;
|
|
6348
|
+
}
|
|
6349
|
+
const report = commitsBeforeFailureReport(commits);
|
|
6350
|
+
if (report === undefined)
|
|
6351
|
+
return;
|
|
6352
|
+
const facts = commits.map(commitBeforeFailureFact);
|
|
6353
|
+
appendMandatoryPresentationSuffix(turn, report, facts);
|
|
6354
|
+
for (const fact of facts)
|
|
6355
|
+
joinSettlementFact(turn, fact);
|
|
6206
6356
|
};
|
|
6207
6357
|
// The runtimes own each frame's position. The shell only joins their stack.
|
|
6208
6358
|
const saveProgress = async (starting, step, required = true) => {
|
|
@@ -1490,6 +1490,158 @@ function carriedPreExistingFacts(
|
|
|
1490
1490
|
});
|
|
1491
1491
|
}
|
|
1492
1492
|
|
|
1493
|
+
// DR-089: a turn whose leaf parks in failure still states, as fact, every
|
|
1494
|
+
// commit its steps proved before the failure, so a closing reply never tells
|
|
1495
|
+
// the Boss that nothing was written when a receipt shows otherwise. A commit
|
|
1496
|
+
// the unresolved-effect report already names is not said twice.
|
|
1497
|
+
interface CommitBeforeFailure {
|
|
1498
|
+
readonly baselineHead: string;
|
|
1499
|
+
readonly commitOid: string;
|
|
1500
|
+
/** Every new commit, oldest first, where the step made more than one (DR-088). */
|
|
1501
|
+
readonly commits?: readonly string[];
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
function commitsBeforeFailure(
|
|
1505
|
+
ledger: PlaybookEffectLedger,
|
|
1506
|
+
boundaryPrefixCount: number,
|
|
1507
|
+
unresolvedEffects: readonly PlaybookCaptainUnresolvedEffect[],
|
|
1508
|
+
): readonly CommitBeforeFailure[] {
|
|
1509
|
+
// An entry names a commit by its proven OID, or — for a receipt listing
|
|
1510
|
+
// several commits, which projects as `multiple-commits` (DR-088 §3) — by
|
|
1511
|
+
// its after HEAD, the last of them.
|
|
1512
|
+
const named = new Set(
|
|
1513
|
+
unresolvedEffects.flatMap(({ classification, afterHead, commitOid }) =>
|
|
1514
|
+
commitOid !== undefined
|
|
1515
|
+
? [commitOid]
|
|
1516
|
+
: classification === 'multiple-commits' && afterHead !== undefined
|
|
1517
|
+
? [afterHead]
|
|
1518
|
+
: [],
|
|
1519
|
+
),
|
|
1520
|
+
);
|
|
1521
|
+
const commits: CommitBeforeFailure[] = [];
|
|
1522
|
+
for (const boundary of ledger.boundaries) {
|
|
1523
|
+
if (boundary.sequence <= boundaryPrefixCount) continue;
|
|
1524
|
+
const receipt = boundary.physicalReceipt;
|
|
1525
|
+
if (
|
|
1526
|
+
receipt?.classification !== 'one-descendant-commit' ||
|
|
1527
|
+
receipt.commitOid === undefined ||
|
|
1528
|
+
named.has(receipt.commitOid)
|
|
1529
|
+
) {
|
|
1530
|
+
continue;
|
|
1531
|
+
}
|
|
1532
|
+
named.add(receipt.commitOid);
|
|
1533
|
+
commits.push({
|
|
1534
|
+
baselineHead: receipt.baseline.head,
|
|
1535
|
+
commitOid: receipt.commitOid,
|
|
1536
|
+
...(receipt.commits === undefined ? {} : { commits: receipt.commits }),
|
|
1537
|
+
});
|
|
1538
|
+
}
|
|
1539
|
+
return commits;
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
/** What one step committed, with every commit where it made several (DR-088). */
|
|
1543
|
+
function commitBeforeFailureClause({
|
|
1544
|
+
baselineHead,
|
|
1545
|
+
commitOid,
|
|
1546
|
+
commits,
|
|
1547
|
+
}: CommitBeforeFailure): string {
|
|
1548
|
+
const made =
|
|
1549
|
+
commits === undefined
|
|
1550
|
+
? 'made one new commit'
|
|
1551
|
+
: `made ${commits.length} new commits, oldest first: ${boundedPathList(commits)}`;
|
|
1552
|
+
return `${made}; HEAD moved from ${baselineHead} to the proven commit ${commitOid}.`;
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
function commitBeforeFailureFact(commit: CommitBeforeFailure): string {
|
|
1556
|
+
return `Before the failure, a step ${commitBeforeFailureClause(commit)}`;
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
function commitsBeforeFailureReport(
|
|
1560
|
+
commits: readonly CommitBeforeFailure[],
|
|
1561
|
+
): string | undefined {
|
|
1562
|
+
if (commits.length === 0) return undefined;
|
|
1563
|
+
return [
|
|
1564
|
+
'Repository changes made before the failure:',
|
|
1565
|
+
...commits.map(
|
|
1566
|
+
(commit) => `- A step ${commitBeforeFailureClause(commit)}`,
|
|
1567
|
+
),
|
|
1568
|
+
'The failure does not undo these commits.',
|
|
1569
|
+
].join('\n');
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
/**
|
|
1573
|
+
* DR-088 §2: whether a final text names one commit as a word, by its full OID
|
|
1574
|
+
* or by an abbreviation of at least seven leading hex digits — the
|
|
1575
|
+
* reconciler's own designation rule, read here to say what it accepted.
|
|
1576
|
+
*/
|
|
1577
|
+
function finalTextNamesCommit(finalText: string, oid: string): boolean {
|
|
1578
|
+
for (const [word] of finalText.matchAll(
|
|
1579
|
+
/(?<![0-9A-Za-z_])[0-9A-Fa-f]{7,64}(?![0-9A-Za-z_])/g,
|
|
1580
|
+
)) {
|
|
1581
|
+
if (oid.startsWith(word.toLowerCase())) return true;
|
|
1582
|
+
}
|
|
1583
|
+
return false;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
/**
|
|
1587
|
+
* DR-088 §3: what the steps completed after `boundaryPrefixCount` committed,
|
|
1588
|
+
* one statement each, in boundary sequence order: every new commit where a
|
|
1589
|
+
* receipt proves more than one, and the commit already at HEAD where a step
|
|
1590
|
+
* whose outcomes all commit or ask named it over an `unchanged` receipt. A
|
|
1591
|
+
* step in a deferred chain reads the chain's cumulative receipt at its last
|
|
1592
|
+
* boundary.
|
|
1593
|
+
*/
|
|
1594
|
+
function stepCommitStatements(
|
|
1595
|
+
ledger: PlaybookEffectLedger,
|
|
1596
|
+
boundaryPrefixCount: number,
|
|
1597
|
+
): readonly string[] {
|
|
1598
|
+
const statements: string[] = [];
|
|
1599
|
+
for (const boundary of ledger.boundaries) {
|
|
1600
|
+
if (boundary.sequence <= boundaryPrefixCount) continue;
|
|
1601
|
+
let receipt = boundary.physicalReceipt;
|
|
1602
|
+
if (boundary.logicalOperationId !== undefined) {
|
|
1603
|
+
const operation = ledger.logicalOperations.find(
|
|
1604
|
+
({ operationId }) => operationId === boundary.logicalOperationId,
|
|
1605
|
+
);
|
|
1606
|
+
if (operation?.boundaryIds.at(-1) !== boundary.boundaryId) continue;
|
|
1607
|
+
receipt = operation.logicalReceipt;
|
|
1608
|
+
}
|
|
1609
|
+
if (receipt === undefined) continue;
|
|
1610
|
+
if (
|
|
1611
|
+
receipt.classification === 'one-descendant-commit' &&
|
|
1612
|
+
receipt.commits !== undefined
|
|
1613
|
+
) {
|
|
1614
|
+
statements.push(
|
|
1615
|
+
`The step made ${receipt.commits.length} new commits, oldest first: ${boundedPathList(receipt.commits)}.`,
|
|
1616
|
+
);
|
|
1617
|
+
continue;
|
|
1618
|
+
}
|
|
1619
|
+
const candidate = boundary.semanticCandidate;
|
|
1620
|
+
const guard =
|
|
1621
|
+
candidate !== null &&
|
|
1622
|
+
typeof candidate === 'object' &&
|
|
1623
|
+
!Array.isArray(candidate)
|
|
1624
|
+
? (candidate as { readonly [key: string]: JsonValue }).guard
|
|
1625
|
+
: undefined;
|
|
1626
|
+
const head = receipt.after?.head;
|
|
1627
|
+
if (
|
|
1628
|
+
receipt.classification === 'unchanged' &&
|
|
1629
|
+
head !== undefined &&
|
|
1630
|
+
!/^0+$/.test(head) &&
|
|
1631
|
+
boundary.dispositions.includes('one-descendant-commit') &&
|
|
1632
|
+
!boundary.dispositions.includes('unchanged') &&
|
|
1633
|
+
typeof guard === 'string' &&
|
|
1634
|
+
guard !== BOSS_QUESTION_OUTCOME &&
|
|
1635
|
+
finalTextNamesCommit(boundary.finalText?.trim() ?? '', head)
|
|
1636
|
+
) {
|
|
1637
|
+
statements.push(
|
|
1638
|
+
`The step made no new commit; it named ${head}, already at HEAD, as its work.`,
|
|
1639
|
+
);
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
return statements;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1493
1645
|
// DR-063 §4: the Boss-visible failure report. Every host sees the same
|
|
1494
1646
|
// sentences because the shell composes them from the structured cause instead
|
|
1495
1647
|
// of asking a model to describe facts it may drop. The report exposes no file
|
|
@@ -4250,6 +4402,15 @@ const unresolvedEffectFromReceipt = (
|
|
|
4250
4402
|
baselineHead = receipt.baseline.head,
|
|
4251
4403
|
): PlaybookCaptainUnresolvedEffect | undefined => {
|
|
4252
4404
|
if (receipt.classification === 'unchanged') return undefined;
|
|
4405
|
+
// DR-088 §3: a receipt listing more than one new commit reads as
|
|
4406
|
+
// `multiple-commits`, whose entry names more than one commit and no OID.
|
|
4407
|
+
if (receipt.commits !== undefined) {
|
|
4408
|
+
return {
|
|
4409
|
+
classification: 'multiple-commits',
|
|
4410
|
+
baselineHead,
|
|
4411
|
+
...(receipt.after === undefined ? {} : { afterHead: receipt.after.head }),
|
|
4412
|
+
};
|
|
4413
|
+
}
|
|
4253
4414
|
return {
|
|
4254
4415
|
classification: receipt.classification,
|
|
4255
4416
|
baselineHead,
|
|
@@ -4352,12 +4513,18 @@ const cumulativeOpenLogicalEffect = (
|
|
|
4352
4513
|
receipt.classification === 'one-descendant-commit' &&
|
|
4353
4514
|
(sameProjection || receiptAccountsFromOriginal)
|
|
4354
4515
|
) {
|
|
4355
|
-
return
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4516
|
+
return receipt.commits === undefined
|
|
4517
|
+
? {
|
|
4518
|
+
classification: 'one-descendant-commit',
|
|
4519
|
+
baselineHead: original.head,
|
|
4520
|
+
afterHead: after.head,
|
|
4521
|
+
commitOid: after.head,
|
|
4522
|
+
}
|
|
4523
|
+
: {
|
|
4524
|
+
classification: 'multiple-commits',
|
|
4525
|
+
baselineHead: original.head,
|
|
4526
|
+
afterHead: after.head,
|
|
4527
|
+
};
|
|
4361
4528
|
}
|
|
4362
4529
|
return {
|
|
4363
4530
|
classification: 'observation-ambiguous',
|
|
@@ -4772,19 +4939,33 @@ export function createPlaybookCaptainShell(
|
|
|
4772
4939
|
const reportCarriedPreExistingChanges = (turn: ActiveTurn): void => {
|
|
4773
4940
|
if (turn.carriedEffectPrefix !== undefined) return;
|
|
4774
4941
|
let carried: readonly CarriedPreExistingChanges[];
|
|
4942
|
+
let commitStatements: readonly string[];
|
|
4775
4943
|
try {
|
|
4776
4944
|
const ledger = assertPlaybookEffectLedger(currentEffectLedger());
|
|
4777
4945
|
carried = carriedPreExistingChanges(ledger, presentedEffectPrefix);
|
|
4946
|
+
commitStatements = stepCommitStatements(ledger, presentedEffectPrefix);
|
|
4778
4947
|
turn.carriedEffectPrefix = ledger.boundaries.length;
|
|
4779
4948
|
} catch {
|
|
4780
4949
|
return;
|
|
4781
4950
|
}
|
|
4782
4951
|
const report = carriedPreExistingReport(carried);
|
|
4783
|
-
if (report
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4952
|
+
if (report !== undefined) {
|
|
4953
|
+
const facts = carriedPreExistingFacts(carried);
|
|
4954
|
+
appendMandatoryPresentationSuffix(turn, report, facts);
|
|
4955
|
+
for (const fact of facts) {
|
|
4956
|
+
joinSettlementFact(turn, fact);
|
|
4957
|
+
}
|
|
4958
|
+
}
|
|
4959
|
+
// DR-088 §3: the commits each step took, read from the same boundaries.
|
|
4960
|
+
if (commitStatements.length > 0) {
|
|
4961
|
+
appendMandatoryPresentationSuffix(
|
|
4962
|
+
turn,
|
|
4963
|
+
commitStatements.join('\n'),
|
|
4964
|
+
commitStatements,
|
|
4965
|
+
);
|
|
4966
|
+
for (const fact of commitStatements) {
|
|
4967
|
+
joinSettlementFact(turn, fact);
|
|
4968
|
+
}
|
|
4788
4969
|
}
|
|
4789
4970
|
};
|
|
4790
4971
|
|
|
@@ -8965,6 +9146,31 @@ export function createPlaybookCaptainShell(
|
|
|
8965
9146
|
controls: failureControlLines(view, fenced),
|
|
8966
9147
|
};
|
|
8967
9148
|
joinSettlementFact(turn, fact, statement?.fact, speak);
|
|
9149
|
+
reportCommitsBeforeFailure(turn, unresolvedEffects);
|
|
9150
|
+
};
|
|
9151
|
+
|
|
9152
|
+
// DR-089: the commits the turn's receipts proved before its leaf parked,
|
|
9153
|
+
// stated beside the failure report and joined to the settlement facts the
|
|
9154
|
+
// closing-reply prompt reads.
|
|
9155
|
+
const reportCommitsBeforeFailure = (
|
|
9156
|
+
turn: ActiveTurn,
|
|
9157
|
+
unresolvedEffects: readonly PlaybookCaptainUnresolvedEffect[],
|
|
9158
|
+
): void => {
|
|
9159
|
+
let commits: readonly CommitBeforeFailure[];
|
|
9160
|
+
try {
|
|
9161
|
+
commits = commitsBeforeFailure(
|
|
9162
|
+
assertPlaybookEffectLedger(currentEffectLedger()),
|
|
9163
|
+
presentedEffectPrefix,
|
|
9164
|
+
unresolvedEffects,
|
|
9165
|
+
);
|
|
9166
|
+
} catch {
|
|
9167
|
+
return;
|
|
9168
|
+
}
|
|
9169
|
+
const report = commitsBeforeFailureReport(commits);
|
|
9170
|
+
if (report === undefined) return;
|
|
9171
|
+
const facts = commits.map(commitBeforeFailureFact);
|
|
9172
|
+
appendMandatoryPresentationSuffix(turn, report, facts);
|
|
9173
|
+
for (const fact of facts) joinSettlementFact(turn, fact);
|
|
8968
9174
|
};
|
|
8969
9175
|
|
|
8970
9176
|
// The runtimes own each frame's position. The shell only joins their stack.
|
package/slc/link.md
CHANGED
|
@@ -341,6 +341,7 @@ interface PlaybookRepositoryReceipt {
|
|
|
341
341
|
readonly baseline: PlaybookRepositoryObservation;
|
|
342
342
|
readonly after?: PlaybookRepositoryObservation;
|
|
343
343
|
readonly commitOid?: string;
|
|
344
|
+
readonly commits?: readonly string[];
|
|
344
345
|
}
|
|
345
346
|
|
|
346
347
|
type PlaybookRepositoryDisposition =
|
|
@@ -1286,6 +1287,12 @@ runtime-owned payload field; `guard` shall name exactly one outcome declared
|
|
|
1286
1287
|
by both the live result map and `outcomeAuthority`; and every semantic-owned
|
|
1287
1288
|
field shall satisfy the result map's required-field type before any actor
|
|
1288
1289
|
output is delivered.
|
|
1290
|
+
That type is the value shape the compiled FSM's own validator admits for the
|
|
1291
|
+
field: the shared engine reads it once at construction by presenting each
|
|
1292
|
+
governed outcome's accepting arm with sample values of a closed set of shapes,
|
|
1293
|
+
a field whose shape the FSM admits as a string or does not reveal stays a
|
|
1294
|
+
string, and the judge prompt and validation error name every non-string shape
|
|
1295
|
+
([DR-089](../specs/decisions/089-judge-replies-take-the-machines-shapes.md)).
|
|
1289
1296
|
The judge prompt shall render each governed outcome's description with its
|
|
1290
1297
|
meaning verbatim and its `Output shall include` clause replaced by that reply
|
|
1291
1298
|
contract — exactly `guard` plus the semantic-owned fields, each keeping its
|
|
@@ -1320,10 +1327,16 @@ physical receipt, or the complete cumulative logical receipt of a deferred
|
|
|
1320
1327
|
operation, whose classification is exactly the outcome's declared
|
|
1321
1328
|
`unchanged` or `one-descendant-commit` disposition; a `one-descendant-commit`
|
|
1322
1329
|
receipt shall carry exactly the after-HEAD OID used for the arm's effect-owned
|
|
1323
|
-
fields,
|
|
1324
|
-
|
|
1325
|
-
HEAD
|
|
1326
|
-
|
|
1330
|
+
fields, and lists every new commit in `commits` where HEAD advanced by more
|
|
1331
|
+
than one, while an `unchanged` receipt's complete validated observation
|
|
1332
|
+
supplies its observed HEAD OID for them, and a receipt that cannot prove that
|
|
1333
|
+
observed HEAD shall leave the envelope unresolved rather than inject a
|
|
1334
|
+
fabricated value.
|
|
1335
|
+
A `one-descendant-commit` outcome shall also resolve over an `unchanged`
|
|
1336
|
+
receipt whose HEAD names a commit when no outcome of that call declares
|
|
1337
|
+
`unchanged` and the trimmed `finalText` names that commit as a word, by its
|
|
1338
|
+
full OID or an abbreviation of at least seven leading hex digits; its
|
|
1339
|
+
effect-owned fields then take that HEAD OID.
|
|
1327
1340
|
A `deferred` candidate shall be admissible only for its already-validated
|
|
1328
1341
|
effect-authorized `needsBossReply` outcome and only from a complete after
|
|
1329
1342
|
observation whose HEAD equals the logical operation's original baseline HEAD
|
|
@@ -1373,10 +1386,16 @@ player or judge call.
|
|
|
1373
1386
|
The adjudicator shall use the same document-order tolerant JSON recovery as
|
|
1374
1387
|
the Boss classifier. Unlike invalid classification, a reply from which no
|
|
1375
1388
|
object can be recovered, an undeclared guard, or a missing required field is a
|
|
1376
|
-
control-plane error for nongoverned delegated-player
|
|
1389
|
+
control-plane error for nongoverned delegated-player adjudication and
|
|
1377
1390
|
shall throw after the invocation reaches its FSM error path and ordered
|
|
1378
1391
|
emissions drain; schema-3 governed adjudication follows the bounded
|
|
1379
1392
|
reconciliation contract above instead.
|
|
1393
|
+
Direct-Captain adjudication treats such a reply, or one carrying an undeclared
|
|
1394
|
+
field, as a structural error: it makes exactly one corrective adjudication over
|
|
1395
|
+
the same visible output that restates the reply and its validation error, and
|
|
1396
|
+
a second structural error routes the invocation to its FSM failure state as a
|
|
1397
|
+
recoverable `judge-failed` failure rather than a control-plane error
|
|
1398
|
+
([DR-089](../specs/decisions/089-judge-replies-take-the-machines-shapes.md)).
|
|
1380
1399
|
|
|
1381
1400
|
Two default adjudication strategies, in selection order:
|
|
1382
1401
|
|
|
@@ -1389,7 +1408,11 @@ Two default adjudication strategies, in selection order:
|
|
|
1389
1408
|
`{ guard, …semanticOwnedPayloadFields }` and explicitly forbids every other
|
|
1390
1409
|
payload field. The governed form also permits the blocked report above when no outcome matches.
|
|
1391
1410
|
Both forms exclude the runtime-injected direct-Captain
|
|
1392
|
-
`question` and `response` fields above.
|
|
1411
|
+
`question` and `response` fields above. A direct-Captain prompt keeps each
|
|
1412
|
+
description and states under it the exact reply that key takes — `guard`
|
|
1413
|
+
plus the structural fields its `Output shall include` clause names, each with
|
|
1414
|
+
its authored placeholder — naming `question` and `response` as
|
|
1415
|
+
runtime-supplied. The prompt shall identify hidden control work,
|
|
1393
1416
|
prohibit tool use, file inspection, and external evidence, direct the judge
|
|
1394
1417
|
to decide only from the supplied actor output and declared outcomes, and
|
|
1395
1418
|
require exactly one JSON object with no prose. The judge prompt shall not
|