@sublang/playbook 14.0.0 → 15.0.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.
Files changed (51) hide show
  1. package/README.md +1 -1
  2. package/docs/cli.md +14 -14
  3. package/docs/configuration.md +15 -9
  4. package/package.json +1 -1
  5. package/reference/sdlc/branch.playbook/branch.fsm.d.ts +1 -1
  6. package/reference/sdlc/code.md +1 -1
  7. package/reference/sdlc/code.playbook/bin/launch-config.js +55 -16
  8. package/reference/sdlc/code.playbook/bin/playbook.js +14 -9
  9. package/reference/sdlc/code.playbook/bin/repository-effects.js +187 -14
  10. package/reference/sdlc/code.playbook/bin/run.js +12 -8
  11. package/reference/sdlc/code.playbook/code.fsm.d.ts +1 -1
  12. package/reference/sdlc/code.playbook/code.fsm.js +1 -1
  13. package/reference/sdlc/code.playbook/code.fsm.ts +1 -1
  14. package/reference/sdlc/code.playbook/code.gears.md +2 -2
  15. package/reference/sdlc/code.playbook/host-capabilities.d.ts +6 -0
  16. package/reference/sdlc/code.playbook/playbook-captain.d.ts +3 -1
  17. package/reference/sdlc/code.playbook/playbook-captain.js +357 -16
  18. package/reference/sdlc/code.playbook/playbook-captain.ts +459 -34
  19. package/reference/sdlc/code.playbook/session-host.d.ts +6 -3
  20. package/reference/sdlc/decide.md +1 -1
  21. package/reference/sdlc/decide.playbook/decide.fsm.d.ts +1 -1
  22. package/reference/sdlc/decide.playbook/decide.fsm.js +1 -1
  23. package/reference/sdlc/decide.playbook/decide.fsm.ts +1 -1
  24. package/reference/sdlc/decide.playbook/decide.gears.md +1 -1
  25. package/reference/sdlc/decide.playbook/decide.playbook.js +25 -1
  26. package/reference/sdlc/decide.playbook/decide.playbook.ts +35 -2
  27. package/reference/sdlc/dev.md +15 -8
  28. package/reference/sdlc/dev.playbook/dev.fsm.d.ts +1 -1
  29. package/reference/sdlc/dev.playbook/dev.fsm.js +18 -12
  30. package/reference/sdlc/dev.playbook/dev.fsm.ts +18 -13
  31. package/reference/sdlc/dev.playbook/dev.gears.md +19 -12
  32. package/reference/sdlc/dev.playbook/dev.playbook.js +1 -1
  33. package/reference/sdlc/dev.playbook/dev.playbook.ts +1 -2
  34. package/reference/sdlc/dev.playbook/dev.registry.js +1 -1
  35. package/reference/sdlc/dev.playbook/dev.registry.ts +1 -1
  36. package/reference/sdlc/pr.playbook/pr.fsm.d.ts +1 -1
  37. package/reference/sdlc/review.md +1 -1
  38. package/reference/sdlc/review.playbook/review.fsm.d.ts +1 -1
  39. package/reference/sdlc/review.playbook/review.fsm.js +1 -1
  40. package/reference/sdlc/review.playbook/review.fsm.ts +1 -1
  41. package/reference/sdlc/review.playbook/review.gears.md +1 -1
  42. package/slc/link.md +32 -0
  43. package/src/runtime.d.ts +70 -0
  44. package/src/runtime.js +232 -1
  45. package/src/runtime.ts +358 -1
  46. package/src/xstate-playbook-runtime.d.ts +9 -10
  47. package/src/xstate-playbook-runtime.js +372 -20
  48. package/src/xstate-playbook-runtime.ts +460 -19
  49. package/src/xstate-runtime.d.ts +9 -1
  50. package/src/xstate-runtime.js +299 -19
  51. package/src/xstate-runtime.ts +362 -21
package/src/runtime.d.ts CHANGED
@@ -32,10 +32,65 @@ export interface CaptainResult {
32
32
  export type JsonValue = null | boolean | number | string | readonly JsonValue[] | {
33
33
  readonly [key: string]: JsonValue;
34
34
  };
35
+ export declare const PLAYBOOK_FAILURE_CODES: readonly ["commit-missing", "commit-residual", "pre-existing-lost", "commits-more-than-one", "history-rewritten", "foreign-change", "observation-unstable", "attribution-ambiguous", "receipt-missing", "judge-failed", "player-failed", "aborted", "child-failed", "runtime-defect"];
36
+ export type PlaybookFailureCode = (typeof PLAYBOOK_FAILURE_CODES)[number];
37
+ /** The bounded path lists one failure code's evidence may carry (DR-063 §1). */
38
+ export interface PlaybookFailurePaths {
39
+ /** Paths changed in the worktree that no commit carries. */
40
+ readonly uncommitted?: readonly string[];
41
+ /** Pre-existing baseline paths the call changed before committing. */
42
+ readonly altered?: readonly string[];
43
+ /** Pre-existing baseline paths whose content is nowhere. */
44
+ readonly lost?: readonly string[];
45
+ /** Paths whose entry differs or is missing on either side. */
46
+ readonly changed?: readonly string[];
47
+ /** Paths omitted from the lists above by the 32-path bound. */
48
+ readonly truncated?: number;
49
+ }
50
+ /** The bounded error record a failure cause may carry (DR-063 §1). */
51
+ export interface PlaybookFailureErrorEvidence {
52
+ readonly name: string;
53
+ readonly message: string;
54
+ }
55
+ /** The closed evidence object of one failure cause (DR-063 §1). */
56
+ export interface PlaybookFailureEvidence {
57
+ readonly required?: PlaybookRepositoryDisposition;
58
+ readonly observed?: PlaybookRepositoryReceipt['classification'];
59
+ readonly baselineHead?: string;
60
+ readonly afterHead?: string;
61
+ readonly commitOid?: string;
62
+ readonly paths?: PlaybookFailurePaths;
63
+ readonly reason?: string;
64
+ readonly error?: PlaybookFailureErrorEvidence;
65
+ readonly errorCode?: string;
66
+ readonly roleId?: string;
67
+ readonly playerId?: string;
68
+ readonly playbookId?: string;
69
+ readonly cause?: PlaybookFailureCause;
70
+ }
71
+ /**
72
+ * Why a parked workflow failed (DR-063 §1): one code from the closed list and
73
+ * the closed evidence object that code names. Evidence holds repository paths,
74
+ * dispositions, classifications, and revision identities only — never file
75
+ * content, player prose, or internal call and session identities.
76
+ */
77
+ export interface PlaybookFailureCause {
78
+ readonly code: PlaybookFailureCode;
79
+ readonly evidence: PlaybookFailureEvidence;
80
+ }
81
+ /**
82
+ * Validate, detach, and freeze one failure cause (DR-063 §1). The check is
83
+ * closed per code: exactly the evidence members that code names, nothing else,
84
+ * and a `child-failed` cause nests at most four deep. A value this rejects is
85
+ * not a cause, so a caller omits it rather than publishing an invented one.
86
+ */
87
+ export declare function assertPlaybookFailureCause(value: unknown): PlaybookFailureCause;
35
88
  export interface NormalizedError {
36
89
  name: string;
37
90
  message: string;
38
91
  stack?: string;
92
+ /** DR-063 §2: present exactly when the underlying error carried a valid one. */
93
+ cause?: PlaybookFailureCause;
39
94
  }
40
95
  export type PlaybookStateValue = string | {
41
96
  readonly [key: string]: PlaybookStateValue;
@@ -181,12 +236,23 @@ export interface PlaybookRepositoryObservation {
181
236
  readonly projection: Readonly<Record<string, JsonValue>>;
182
237
  readonly projectionDigest: string;
183
238
  }
239
+ /**
240
+ * The fate of the baseline projection entries a governed call absorbed into
241
+ * its one commit, altered, or lost (DR-062). Each list holds sorted unique
242
+ * baseline paths and the three are pairwise disjoint.
243
+ */
244
+ export interface PlaybookRepositoryPreExistingChanges {
245
+ readonly absorbed: readonly string[];
246
+ readonly altered: readonly string[];
247
+ readonly lost: readonly string[];
248
+ }
184
249
  /** The fail-closed classification of one complete physical or logical receipt. */
185
250
  export interface PlaybookRepositoryReceipt {
186
251
  readonly classification: 'unchanged' | 'one-descendant-commit' | 'multiple-commits' | 'rewritten-or-non-descendant' | 'worktree-only-change' | 'concurrent-or-foreign-change' | 'observation-ambiguous';
187
252
  readonly baseline: PlaybookRepositoryObservation;
188
253
  readonly after?: PlaybookRepositoryObservation;
189
254
  readonly commitOid?: string;
255
+ readonly preExisting?: PlaybookRepositoryPreExistingChanges;
190
256
  }
191
257
  /** One durably ordered physical governed-player boundary (DR-040). */
192
258
  export interface PlaybookEffectBoundary {
@@ -324,9 +390,13 @@ export interface PlaybookRuntimeSnapshot {
324
390
  };
325
391
  suspendedCall?: PlaybookSuspendedCall;
326
392
  }
393
+ export type PlaybookControlStanding = 'ready' | 'no-op' | 'blocked';
394
+ export type PlaybookControlActionReason = 'receipt-complete';
327
395
  export interface PlaybookControlAction {
328
396
  id: string;
329
397
  label: string;
398
+ standing?: PlaybookControlStanding;
399
+ reason?: PlaybookControlActionReason;
330
400
  }
331
401
  export interface PlaybookControlView {
332
402
  state: PlaybookState;
package/src/runtime.js CHANGED
@@ -7,4 +7,235 @@
7
7
  // one way: linked playbook runtimes (e.g. code.playbook.ts) import and
8
8
  // re-export these names rather than redefining them
9
9
  // (PBRT-5, PBRT-34, DR-004 Addendum A4).
10
- export {};
10
+ // DR-063 §1: the closed list of failure codes. There is no unknown code — an
11
+ // unrecognized failure is `runtime-defect` carrying its message as `reason` —
12
+ // so a host catalogue is complete exactly when it covers this list.
13
+ export const PLAYBOOK_FAILURE_CODES = [
14
+ 'commit-missing',
15
+ 'commit-residual',
16
+ 'pre-existing-lost',
17
+ 'commits-more-than-one',
18
+ 'history-rewritten',
19
+ 'foreign-change',
20
+ 'observation-unstable',
21
+ 'attribution-ambiguous',
22
+ 'receipt-missing',
23
+ 'judge-failed',
24
+ 'player-failed',
25
+ 'aborted',
26
+ 'child-failed',
27
+ 'runtime-defect',
28
+ ];
29
+ /** Evidence members admitted for each code, in the order a sentence reads them. */
30
+ const FAILURE_EVIDENCE_MEMBERS = {
31
+ 'commit-missing': {
32
+ required: ['required', 'observed', 'baselineHead', 'afterHead', 'paths'],
33
+ optional: [],
34
+ paths: ['uncommitted'],
35
+ },
36
+ 'commit-residual': {
37
+ required: ['required', 'observed', 'baselineHead', 'afterHead', 'paths'],
38
+ optional: ['commitOid'],
39
+ paths: ['uncommitted', 'altered'],
40
+ },
41
+ 'pre-existing-lost': {
42
+ required: ['required', 'observed', 'baselineHead', 'paths'],
43
+ optional: ['afterHead'],
44
+ paths: ['lost'],
45
+ },
46
+ 'commits-more-than-one': {
47
+ required: ['required', 'observed', 'baselineHead', 'afterHead'],
48
+ optional: [],
49
+ paths: [],
50
+ },
51
+ 'history-rewritten': {
52
+ required: ['required', 'observed', 'baselineHead', 'afterHead'],
53
+ optional: [],
54
+ paths: [],
55
+ },
56
+ 'foreign-change': {
57
+ required: ['required', 'observed', 'baselineHead', 'afterHead', 'paths'],
58
+ optional: [],
59
+ paths: ['changed'],
60
+ },
61
+ 'observation-unstable': {
62
+ required: ['required', 'observed', 'baselineHead'],
63
+ optional: [],
64
+ paths: [],
65
+ },
66
+ 'attribution-ambiguous': {
67
+ required: ['required', 'observed', 'baselineHead'],
68
+ optional: ['afterHead'],
69
+ paths: [],
70
+ },
71
+ 'receipt-missing': { required: ['baselineHead'], optional: [], paths: [] },
72
+ 'judge-failed': { required: ['reason'], optional: ['error'], paths: [] },
73
+ 'player-failed': {
74
+ required: ['roleId', 'error'],
75
+ optional: ['playerId', 'errorCode'],
76
+ paths: [],
77
+ },
78
+ aborted: { required: [], optional: [], paths: [] },
79
+ 'child-failed': { required: ['playbookId'], optional: ['cause'], paths: [] },
80
+ 'runtime-defect': { required: ['reason'], optional: [], paths: [] },
81
+ };
82
+ const FAILURE_PATH_LIMIT = 32;
83
+ const FAILURE_CAUSE_DEPTH_LIMIT = 4;
84
+ const REPOSITORY_DISPOSITIONS = [
85
+ 'unchanged',
86
+ 'one-descendant-commit',
87
+ 'deferred',
88
+ ];
89
+ const RECEIPT_CLASSIFICATIONS = [
90
+ 'unchanged',
91
+ 'one-descendant-commit',
92
+ 'multiple-commits',
93
+ 'rewritten-or-non-descendant',
94
+ 'worktree-only-change',
95
+ 'concurrent-or-foreign-change',
96
+ 'observation-ambiguous',
97
+ ];
98
+ function failureCauseRecord(value, path) {
99
+ if (value === null ||
100
+ typeof value !== 'object' ||
101
+ Array.isArray(value) ||
102
+ (Object.getPrototypeOf(value) !== Object.prototype &&
103
+ Object.getPrototypeOf(value) !== null)) {
104
+ throw new TypeError(`${path} must be a plain object`);
105
+ }
106
+ return value;
107
+ }
108
+ function failureCauseString(value, path) {
109
+ if (typeof value !== 'string' || value.length === 0) {
110
+ throw new TypeError(`${path} must be a nonempty string`);
111
+ }
112
+ return value;
113
+ }
114
+ function failureCausePathList(value, path) {
115
+ if (!Array.isArray(value))
116
+ throw new TypeError(`${path} must be an array`);
117
+ if (value.length > FAILURE_PATH_LIMIT) {
118
+ throw new TypeError(`${path} must hold at most ${FAILURE_PATH_LIMIT} paths`);
119
+ }
120
+ const paths = value.map((entry, index) => failureCauseString(entry, `${path}[${index}]`));
121
+ for (const [index, entry] of paths.entries()) {
122
+ if (index > 0 && !(paths[index - 1] < entry)) {
123
+ throw new TypeError(`${path} must be sorted and free of duplicates`);
124
+ }
125
+ }
126
+ return Object.freeze(paths);
127
+ }
128
+ function failureCausePaths(value, admitted, path) {
129
+ const record = failureCauseRecord(value, path);
130
+ const allowed = new Set([...admitted, 'truncated']);
131
+ for (const key of Object.keys(record)) {
132
+ if (!allowed.has(key)) {
133
+ throw new TypeError(`${path} must not carry ${JSON.stringify(key)}`);
134
+ }
135
+ }
136
+ const paths = {};
137
+ for (const member of admitted) {
138
+ if (!Object.hasOwn(record, member)) {
139
+ throw new TypeError(`${path}.${member} is required`);
140
+ }
141
+ paths[member] = failureCausePathList(record[member], `${path}.${member}`);
142
+ }
143
+ if (Object.hasOwn(record, 'truncated')) {
144
+ const truncated = record.truncated;
145
+ if (typeof truncated !== 'number' ||
146
+ !Number.isSafeInteger(truncated) ||
147
+ truncated <= 0) {
148
+ throw new TypeError(`${path}.truncated must be a positive integer`);
149
+ }
150
+ paths.truncated = truncated;
151
+ }
152
+ return Object.freeze(paths);
153
+ }
154
+ function failureCauseErrorEvidence(value, path) {
155
+ const record = failureCauseRecord(value, path);
156
+ for (const key of Object.keys(record)) {
157
+ if (key !== 'name' && key !== 'message') {
158
+ throw new TypeError(`${path} must not carry ${JSON.stringify(key)}`);
159
+ }
160
+ }
161
+ if (typeof record.message !== 'string') {
162
+ throw new TypeError(`${path}.message must be a string`);
163
+ }
164
+ return Object.freeze({
165
+ name: failureCauseString(record.name, `${path}.name`),
166
+ message: record.message,
167
+ });
168
+ }
169
+ function validateFailureCause(value, path, depth) {
170
+ if (depth > FAILURE_CAUSE_DEPTH_LIMIT) {
171
+ throw new TypeError(`${path} must nest at most ${FAILURE_CAUSE_DEPTH_LIMIT} causes`);
172
+ }
173
+ const record = failureCauseRecord(value, path);
174
+ for (const key of Object.keys(record)) {
175
+ if (key !== 'code' && key !== 'evidence') {
176
+ throw new TypeError(`${path} must not carry ${JSON.stringify(key)}`);
177
+ }
178
+ }
179
+ const code = record.code;
180
+ if (typeof code !== 'string' ||
181
+ !PLAYBOOK_FAILURE_CODES.includes(code)) {
182
+ throw new TypeError(`${path}.code must be one of the declared codes`);
183
+ }
184
+ const spec = FAILURE_EVIDENCE_MEMBERS[code];
185
+ const evidenceRecord = failureCauseRecord(record.evidence, `${path}.evidence`);
186
+ const allowed = new Set([...spec.required, ...spec.optional]);
187
+ for (const key of Object.keys(evidenceRecord)) {
188
+ if (!allowed.has(key)) {
189
+ throw new TypeError(`${path}.evidence must not carry ${JSON.stringify(key)}`);
190
+ }
191
+ }
192
+ const evidence = {};
193
+ for (const member of [...spec.required, ...spec.optional]) {
194
+ const required = spec.required.includes(member);
195
+ if (!Object.hasOwn(evidenceRecord, member)) {
196
+ if (required) {
197
+ throw new TypeError(`${path}.evidence.${member} is required`);
198
+ }
199
+ continue;
200
+ }
201
+ const memberValue = evidenceRecord[member];
202
+ const memberPath = `${path}.evidence.${member}`;
203
+ if (member === 'required') {
204
+ if (!REPOSITORY_DISPOSITIONS.includes(memberValue)) {
205
+ throw new TypeError(`${memberPath} must be a repository disposition`);
206
+ }
207
+ evidence[member] = memberValue;
208
+ }
209
+ else if (member === 'observed') {
210
+ if (!RECEIPT_CLASSIFICATIONS.includes(memberValue)) {
211
+ throw new TypeError(`${memberPath} must be a receipt classification`);
212
+ }
213
+ evidence[member] = memberValue;
214
+ }
215
+ else if (member === 'paths') {
216
+ evidence[member] = failureCausePaths(memberValue, spec.paths, memberPath);
217
+ }
218
+ else if (member === 'error') {
219
+ evidence[member] = failureCauseErrorEvidence(memberValue, memberPath);
220
+ }
221
+ else if (member === 'cause') {
222
+ evidence[member] = validateFailureCause(memberValue, memberPath, depth + 1);
223
+ }
224
+ else {
225
+ evidence[member] = failureCauseString(memberValue, memberPath);
226
+ }
227
+ }
228
+ return Object.freeze({
229
+ code: code,
230
+ evidence: Object.freeze(evidence),
231
+ });
232
+ }
233
+ /**
234
+ * Validate, detach, and freeze one failure cause (DR-063 §1). The check is
235
+ * closed per code: exactly the evidence members that code names, nothing else,
236
+ * and a `child-failed` cause nests at most four deep. A value this rejects is
237
+ * not a cause, so a caller omits it rather than publishing an invented one.
238
+ */
239
+ export function assertPlaybookFailureCause(value) {
240
+ return validateFailureCause(value, 'playbook failure cause', 1);
241
+ }