@sublang/playbook 15.0.0 → 16.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 (81) hide show
  1. package/README.md +1 -1
  2. package/docs/embedding.md +9 -7
  3. package/package.json +3 -1
  4. package/reference/sdlc/branch.playbook/branch.fsm.d.ts +63 -33
  5. package/reference/sdlc/branch.playbook/branch.fsm.js +190 -108
  6. package/reference/sdlc/branch.playbook/branch.fsm.ts +269 -147
  7. package/reference/sdlc/branch.playbook/branch.gears.md +7 -17
  8. package/reference/sdlc/branch.playbook/branch.playbook.d.ts +12 -16
  9. package/reference/sdlc/branch.playbook/branch.playbook.js +112 -101
  10. package/reference/sdlc/branch.playbook/branch.playbook.ts +128 -171
  11. package/reference/sdlc/branch.playbook/branch.registry.d.ts +3 -2
  12. package/reference/sdlc/branch.playbook/branch.registry.ts +6 -2
  13. package/reference/sdlc/code.playbook/bin/repository-effects.js +54 -13
  14. package/reference/sdlc/code.playbook/code.fsm.d.ts +143 -90
  15. package/reference/sdlc/code.playbook/code.fsm.introspect.d.ts +8 -8
  16. package/reference/sdlc/code.playbook/code.fsm.introspect.ts +13 -13
  17. package/reference/sdlc/code.playbook/code.fsm.js +609 -649
  18. package/reference/sdlc/code.playbook/code.fsm.ts +890 -821
  19. package/reference/sdlc/code.playbook/code.gears.md +39 -29
  20. package/reference/sdlc/code.playbook/code.playbook.d.ts +13 -16
  21. package/reference/sdlc/code.playbook/code.playbook.js +166 -147
  22. package/reference/sdlc/code.playbook/code.playbook.ts +180 -229
  23. package/reference/sdlc/code.playbook/code.registry.d.ts +3 -2
  24. package/reference/sdlc/code.playbook/code.registry.ts +6 -2
  25. package/reference/sdlc/code.playbook/playbook-captain.d.ts +42 -0
  26. package/reference/sdlc/code.playbook/playbook-captain.js +829 -194
  27. package/reference/sdlc/code.playbook/playbook-captain.ts +1227 -220
  28. package/reference/sdlc/decide.md +1 -0
  29. package/reference/sdlc/decide.playbook/decide.fsm.d.ts +270 -154
  30. package/reference/sdlc/decide.playbook/decide.fsm.js +765 -732
  31. package/reference/sdlc/decide.playbook/decide.fsm.ts +1167 -945
  32. package/reference/sdlc/decide.playbook/decide.gears.md +28 -21
  33. package/reference/sdlc/decide.playbook/decide.playbook.d.ts +11 -67
  34. package/reference/sdlc/decide.playbook/decide.playbook.js +155 -3570
  35. package/reference/sdlc/decide.playbook/decide.playbook.ts +186 -5061
  36. package/reference/sdlc/decide.playbook/decide.registry.d.ts +8 -4
  37. package/reference/sdlc/decide.playbook/decide.registry.js +2 -2
  38. package/reference/sdlc/decide.playbook/decide.registry.ts +13 -6
  39. package/reference/sdlc/dev.playbook/dev.fsm.d.ts +113 -131
  40. package/reference/sdlc/dev.playbook/dev.fsm.js +517 -575
  41. package/reference/sdlc/dev.playbook/dev.fsm.ts +667 -787
  42. package/reference/sdlc/dev.playbook/dev.gears.md +52 -54
  43. package/reference/sdlc/dev.playbook/dev.playbook.d.ts +13 -10
  44. package/reference/sdlc/dev.playbook/dev.playbook.js +128 -120
  45. package/reference/sdlc/dev.playbook/dev.playbook.ts +143 -193
  46. package/reference/sdlc/dev.playbook/dev.registry.d.ts +3 -2
  47. package/reference/sdlc/dev.playbook/dev.registry.ts +6 -2
  48. package/reference/sdlc/pr.playbook/pr.fsm.d.ts +92 -85
  49. package/reference/sdlc/pr.playbook/pr.fsm.js +461 -518
  50. package/reference/sdlc/pr.playbook/pr.fsm.ts +533 -663
  51. package/reference/sdlc/pr.playbook/pr.gears.md +47 -63
  52. package/reference/sdlc/pr.playbook/pr.playbook.d.ts +13 -23
  53. package/reference/sdlc/pr.playbook/pr.playbook.js +118 -112
  54. package/reference/sdlc/pr.playbook/pr.playbook.ts +136 -190
  55. package/reference/sdlc/pr.playbook/pr.registry.d.ts +3 -2
  56. package/reference/sdlc/pr.playbook/pr.registry.ts +4 -2
  57. package/reference/sdlc/review.md +3 -3
  58. package/reference/sdlc/review.playbook/review.fsm.d.ts +150 -116
  59. package/reference/sdlc/review.playbook/review.fsm.js +479 -550
  60. package/reference/sdlc/review.playbook/review.fsm.ts +717 -662
  61. package/reference/sdlc/review.playbook/review.gears.md +32 -29
  62. package/reference/sdlc/review.playbook/review.playbook.d.ts +12 -12
  63. package/reference/sdlc/review.playbook/review.playbook.js +180 -155
  64. package/reference/sdlc/review.playbook/review.playbook.ts +190 -230
  65. package/reference/sdlc/review.playbook/review.registry.d.ts +6 -5
  66. package/reference/sdlc/review.playbook/review.registry.js +3 -3
  67. package/reference/sdlc/review.playbook/review.registry.ts +9 -5
  68. package/slc/gears2fsm.md +72 -3
  69. package/slc/link.md +50 -27
  70. package/slc/materialize-link.mjs +51 -13
  71. package/slc/optimize.md +4 -1
  72. package/slc/prefix-prompts.mjs +191 -0
  73. package/slc/prefix.md +203 -0
  74. package/slc/slc.pin-inputs.json +11 -1
  75. package/slc/text2gears.md +71 -11
  76. package/src/xstate-playbook-runtime.d.ts +82 -9
  77. package/src/xstate-playbook-runtime.js +1224 -283
  78. package/src/xstate-playbook-runtime.ts +1671 -338
  79. package/src/xstate-runtime.d.ts +3 -1
  80. package/src/xstate-runtime.js +9 -2
  81. package/src/xstate-runtime.ts +12 -2
@@ -1,217 +1,174 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
3
- //
4
- // Generated by slc/link.md (FSM-to-Runtime linker).
5
- // Source FSM: ./branch.fsm.ts
6
- // Role metadata: Coder→coder (canonical local role)
7
- // Boss event: deterministic START_BRANCH entry; exact Boss text becomes
8
- // callerInput; pending Coder questions retain BOSS_REPLY
9
- // Adjudication: LLM judge for the one Coder state; coderOutput is carried
10
- // verbatim; baseRevision is receipt-owned effect evidence —
11
- // the unchanged receipt's observed HEAD — never taken from
12
- // Coder's prose
13
- // Compat: artifact schema 3 / runtime ABI 1
14
-
3
+ // Generated by slc/materialize-link.mjs; governed by the complete slc/link.md.
4
+ // FSM: "./branch.fsm.js"; export: branchMachine; strategies: shared defaults with labelled string relays and nested actors.
5
+ import { branchMachine as machine } from "./branch.fsm.js";
6
+ import type { PlayerInput as SourcePlayerInput } from "./branch.fsm.js";
15
7
  import {
16
- createXStatePlaybookRuntime,
17
- composePlayerContinuation,
18
- snapshotJsonValue,
8
+ createXStatePlaybookRuntime, snapshotJsonValue,
9
+ composePlayerContinuation, type XStatePromptIdentity,
19
10
  type PlaybookPlayerInput,
20
- type XStatePlaybookRuntimeFactory,
21
11
  type XStatePlaybookRuntimeConstruction,
12
+ type XStatePlaybookRuntimeFactory,
22
13
  type XStatePlaybookRuntimeSpecV3,
23
14
  } from '@sublang/playbook/xstate-runtime';
24
- import {
25
- branchMachine,
26
- type BranchInput,
27
- type PlayerInput,
28
- } from './branch.fsm.js';
29
- import type { PlaybookHostConstructionCapabilities } from '../code.playbook/playbook-captain.js';
30
- import type {
31
- CaptainCallOptions,
32
- CaptainResult,
33
- JsonValue,
34
- NormalizedError,
35
- PlayerCallOptions,
36
- PlaybookCallRequest,
37
- PlaybookCallResult,
38
- PlaybookCallStart,
39
- PlaybookControlReceipt,
40
- PlaybookControlView,
41
- PlaybookPendingCall,
42
- PlaybookPorts,
43
- PlaybookRunResult,
44
- PlaybookRuntime,
45
- PlaybookRuntimeFactory,
46
- PlaybookRuntimeSnapshot,
47
- PlaybookSession,
48
- PlaybookState,
49
- PlaybookStateValue,
50
- PlaybookTraceEvent,
51
- PlaybookTraceType,
15
+ export type {
52
16
  PlayerResult,
17
+ PlayerCallOptions,
53
18
  PlayerSessionStore,
54
- } from '@sublang/playbook/runtime';
55
-
56
- export type {
57
- CaptainCallOptions,
58
19
  CaptainResult,
20
+ CaptainCallOptions,
59
21
  JsonValue,
60
22
  NormalizedError,
61
- PlayerCallOptions,
62
23
  PlaybookCallRequest,
63
24
  PlaybookCallResult,
64
25
  PlaybookCallStart,
65
- PlaybookControlReceipt,
66
- PlaybookControlView,
26
+ PlaybookStateValue,
27
+ PlaybookState,
67
28
  PlaybookPendingCall,
68
- PlaybookPorts,
69
29
  PlaybookRunResult,
70
30
  PlaybookRuntime,
71
31
  PlaybookRuntimeFactory,
72
32
  PlaybookRuntimeSnapshot,
73
33
  PlaybookSession,
74
- PlaybookState,
75
- PlaybookStateValue,
34
+ PlaybookPorts,
76
35
  PlaybookTraceEvent,
77
36
  PlaybookTraceType,
78
- PlayerResult,
79
- PlayerSessionStore,
80
- };
37
+ PlaybookControlReceipt,
38
+ PlaybookControlView,
39
+ } from '@sublang/playbook/runtime';
81
40
 
82
- export type BranchPlaybookOptions = BranchInput;
83
- export type BranchPlaybookHostCapabilities =
84
- PlaybookHostConstructionCapabilities &
85
- XStatePlaybookRuntimeConstruction<BranchPlaybookOptions, object>['hostCapabilities'];
41
+ export interface PlaybookRuntimeOptions {
86
42
 
87
- const PLACEHOLDER = /<(#|[A-Za-z_$][A-Za-z0-9_$-]*)>/g;
43
+ }
44
+ export type PlaybookHostCapabilities = XStatePlaybookRuntimeConstruction<
45
+ PlaybookRuntimeOptions, { readonly authority: object }
46
+ >['hostCapabilities'];
88
47
 
48
+ const OPTION_SCHEMA = {} as const;
49
+ const INPUT_MAPPING = {} as const;
50
+ const RESUMABLE_STATE_IDS: ReadonlySet<string> = new Set([
51
+ "createBranch"
52
+ ]);
53
+ const UNFINISHED_FINAL_STATE_IDS: ReadonlySet<string> = new Set([
54
+ "refused"
55
+ ]);
89
56
  const VERBATIM_PAYLOAD_FIELDS: ReadonlySet<string> = new Set([
90
- 'coderOutput',
57
+ "coderOutput"
91
58
  ]);
92
- const UNFINISHED_FINAL_STATE_IDS: ReadonlySet<string> = new Set(['refused']);
93
-
94
- function snapshotBranchOptions(value: unknown): BranchPlaybookOptions {
95
- const captured = snapshotJsonValue(value, 'BRANCH runtime options');
96
- if (
97
- captured === null ||
98
- typeof captured !== 'object' ||
99
- Array.isArray(captured)
100
- ) {
101
- throw new TypeError('BRANCH runtime options must be an object');
59
+ export function validateOptions(value: unknown): PlaybookRuntimeOptions {
60
+ const captured = snapshotJsonValue(value === undefined ? {} : value, "BRANCH runtime options");
61
+ if (captured === null || typeof captured !== 'object' || Array.isArray(captured)) {
62
+ throw new TypeError("BRANCH runtime options must be an object");
102
63
  }
103
- const record = captured as Readonly<Record<string, JsonValue>>;
104
- for (const key of Object.keys(record)) {
105
- throw new TypeError(`BRANCH runtime options.${key} is not declared`);
64
+ const fields = captured as Readonly<Record<string, unknown>>;
65
+ for (const key of Object.keys(captured)) {
66
+ if (!Object.hasOwn(OPTION_SCHEMA, key)) throw new TypeError("BRANCH runtime options." + key + ' is not declared');
106
67
  }
107
- return captured as unknown as BranchPlaybookOptions;
108
- }
109
-
110
- function placeholderField(token: string): string {
111
- if (token === '#') return 'irNumber';
112
- return token.replace(/-([A-Za-z0-9_$])/g, (_match, next: string) =>
113
- next.toUpperCase(),
114
- );
115
- }
116
-
117
- function quotedContinuation(value: string): string {
118
- return value.replaceAll('\n', '\n> ');
68
+ for (const [key, schema] of Object.entries(OPTION_SCHEMA) as [string, { type: string; required: boolean }][]) {
69
+ if (!Object.hasOwn(captured, key)) {
70
+ if (schema.required) throw new TypeError("BRANCH runtime options." + key + ' is required');
71
+ } else if (typeof fields[key] !== schema.type || (schema.type === 'number' && !Number.isFinite(fields[key]))) {
72
+ throw new TypeError("BRANCH runtime options." + key + ' must be a ' + schema.type);
73
+ }
74
+ }
75
+ return captured as unknown as PlaybookRuntimeOptions;
119
76
  }
120
-
121
- /**
122
- * Preserve authored Markdown quote markers around every line of relayed
123
- * runtime text. The generic composer preserves the marker itself; BRANCH's
124
- * override additionally keeps a multiline caller input inside that quote.
125
- */
126
- function composePlayerPrompt(input: PlayerInput, resuming = false): string {
127
- const fields = input as unknown as Record<string, unknown>;
128
- const body = input.prompt.replace(
129
- PLACEHOLDER,
130
- (match: string, token: string, offset: number, source: string) => {
131
- const value = fields[placeholderField(token)];
77
+ function renderPlayerPrompt(input: SourcePlayerInput, promptIdentity: XStatePromptIdentity, resuming = false): string {
78
+ const fields = input as unknown as Readonly<Record<string, unknown>>;
79
+ const mapping: Readonly<Record<string, string>> = {};
80
+ const identities: Readonly<Record<string, string>> = {};
81
+ const omitted = new Set<string>([]);
82
+ const fieldFor = (token: string): string => Object.hasOwn(mapping, token) ? mapping[token]! : (token === '#' ? 'irNumber' : token.replace(/-([A-Za-z0-9_$])/g, (_match: string, next: string) => next.toUpperCase()));
83
+ const parts = input.prompt.split(/(\r?\n)/);
84
+ let body = '';
85
+ for (let index = 0; index < parts.length; index += 2) {
86
+ const line = parts[index]!;
87
+ if (omitted.has(line)) {
88
+ const token = /<(#|[A-Za-z_$][A-Za-z0-9_$-]*)>$/.exec(line)![1]!;
89
+ if (fields[fieldFor(token)] === '') continue;
90
+ }
91
+ body += line.replace(/<(#|[A-Za-z_$][A-Za-z0-9_$-]*)>/g, (match: string, token: string): string => {
92
+ const value = Object.hasOwn(identities, token) ? promptIdentity(identities[token]!) : fields[fieldFor(token)];
132
93
  if (typeof value !== 'string') return match;
133
- const lineStart = source.lastIndexOf('\n', offset - 1) + 1;
134
- const literal = source.slice(lineStart, offset);
135
- return literal.startsWith('> ') ? quotedContinuation(value) : value;
136
- },
137
- );
94
+ return line.startsWith('> ') ? value.replace(/\r?\n/g, (ending: string) => ending + '> ') : value;
95
+ }) + (parts[index + 1] ?? '');
96
+ }
138
97
  return composePlayerContinuation(input, body, resuming);
139
98
  }
140
-
141
- export const _internal = {
142
- composePlayerPrompt,
143
- VERBATIM_PAYLOAD_FIELDS,
144
- UNFINISHED_FINAL_STATE_IDS,
145
- };
146
-
99
+ const composePlayerPrompt = (input: PlaybookPlayerInput, promptIdentity: XStatePromptIdentity, resuming?: boolean): string =>
100
+ renderPlayerPrompt(input as SourcePlayerInput, promptIdentity, resuming);
147
101
  const runtimeSpec = {
148
- label: 'BRANCH',
149
- // DR-022 / slc/link.md: the declaration carries the value current at link
150
- // time — a literal, never the loading engine's RUNTIME_ABI self-report,
151
- // which would follow whatever engine loads the module and make the
152
- // factory's skew check compare that engine with itself.
153
- compat: { artifactSchema: 3, runtimeAbi: 1 },
154
- snapshotOptions: snapshotBranchOptions,
155
- entryEvent: {
156
- type: 'START_BRANCH',
157
- textField: 'callerInput',
158
- // `startBranch` copies the entry text here, so the failure-state retry
159
- // reads it back from the persisted machine snapshot and survives a
160
- // continued session (DR-034).
161
- contextField: 'callerInput',
102
+ composePlayerPrompt,
103
+ ...{
104
+ "label": "BRANCH",
105
+ "compat": {
106
+ "artifactSchema": 3,
107
+ "runtimeAbi": 1
108
+ },
109
+ "entryEvent": {
110
+ "type": "START_BRANCH",
111
+ "textField": "callerInput",
112
+ "contextField": "callerInput"
162
113
  },
163
- roleStates: {
164
- createBranch: {
165
- role: 'coder',
166
- label:
167
- 'Coder is reading the named issue and creating and checking out a new branch at the current commit.',
168
- },
114
+ "bossEvents": [],
115
+ "roleStates": {
116
+ "createBranch": {
117
+ "role": "coder",
118
+ "label": "Coder is identifying the request's issue and creating and checking out a new branch at the current commit."
119
+ }
169
120
  },
170
- // DR-050: BRANCH owns no repository commit — every Coder outcome, including
171
- // the Boss question for an ambiguous request, is governed with the
172
- // `unchanged` repository disposition, because a new branch at the current
173
- // commit changes neither HEAD's OID nor the worktree projection.
174
- outcomeAuthority: {
175
- governedPlayerStates: {
176
- createBranch: {
177
- branched: {
178
- fields: {
179
- branch: 'semantic',
180
- // DR-045: the unchanged receipt observes the commit the branch
181
- // was created from as HEAD; the reconciler injects it here.
182
- baseRevision: 'effect',
183
- issueSummary: 'semantic',
121
+ "outcomeAuthority": {
122
+ "governedPlayerStates": {
123
+ "createBranch": {
124
+ "branched": {
125
+ "fields": {
126
+ "branch": "semantic",
127
+ "baseRevision": "effect",
128
+ "issueSummary": "semantic"
184
129
  },
185
- repositoryDisposition: 'unchanged',
130
+ "repositoryDisposition": "unchanged"
186
131
  },
187
- refused: {
188
- fields: { coderOutput: 'presentation' },
189
- repositoryDisposition: 'unchanged',
190
- },
191
- needsBossReply: {
192
- fields: { question: 'presentation' },
193
- repositoryDisposition: 'unchanged',
132
+ "refused": {
133
+ "fields": {
134
+ "coderOutput": "presentation"
135
+ },
136
+ "repositoryDisposition": "unchanged"
194
137
  },
195
- },
196
- },
138
+ "needsBossReply": {
139
+ "fields": {
140
+ "question": "presentation"
141
+ },
142
+ "repositoryDisposition": "unchanged"
143
+ }
144
+ }
145
+ }
197
146
  },
198
- composePlayerPrompt: (input: PlaybookPlayerInput, _identity, resuming) =>
199
- composePlayerPrompt(input as PlayerInput, resuming),
147
+ "placeholderFields": {},
148
+ "transitionEventFields": [
149
+ "callerInput",
150
+ "answer",
151
+ "questionId"
152
+ ],
153
+ "controlContextFields": []
154
+ },
155
+ snapshotOptions: validateOptions,
156
+ machineInput: (options: PlaybookRuntimeOptions) => Object.fromEntries(
157
+ Object.entries(INPUT_MAPPING).filter(([, key]) => options[key as keyof PlaybookRuntimeOptions] !== undefined)
158
+ .map(([field, key]) => [field, options[key as keyof PlaybookRuntimeOptions]]),
159
+ ),
200
160
  verbatimPayloadFields: VERBATIM_PAYLOAD_FIELDS,
201
- controlContextFields: [],
161
+ resumableStateIds: RESUMABLE_STATE_IDS,
202
162
  unfinishedFinalStateIds: UNFINISHED_FINAL_STATE_IDS,
203
- transitionEventFields: ['callerInput', 'answer', 'questionId'],
204
- } satisfies XStatePlaybookRuntimeSpecV3<BranchPlaybookOptions>;
163
+ } satisfies XStatePlaybookRuntimeSpecV3<PlaybookRuntimeOptions>;
205
164
 
165
+ export const _internal = {
166
+ composePlayerPrompt,
167
+ RESUMABLE_STATE_IDS,
168
+ UNFINISHED_FINAL_STATE_IDS,
169
+ VERBATIM_PAYLOAD_FIELDS,
170
+ };
206
171
  const createPlaybookRuntime: XStatePlaybookRuntimeFactory<
207
- XStatePlaybookRuntimeConstruction<
208
- BranchPlaybookOptions,
209
- BranchPlaybookHostCapabilities
210
- >,
211
- 3
212
- > = createXStatePlaybookRuntime<
213
- BranchPlaybookOptions,
214
- BranchPlaybookHostCapabilities
215
- >(branchMachine, runtimeSpec);
216
-
172
+ XStatePlaybookRuntimeConstruction<PlaybookRuntimeOptions, PlaybookHostCapabilities>, 3
173
+ > = createXStatePlaybookRuntime<PlaybookRuntimeOptions, PlaybookHostCapabilities>(machine, runtimeSpec);
217
174
  export default createPlaybookRuntime;
@@ -1,4 +1,5 @@
1
- import { type BranchPlaybookHostCapabilities, type PlaybookRuntime } from './branch.playbook.js';
1
+ import { type PlaybookHostCapabilities, type PlaybookRuntime } from './branch.playbook.js';
2
+ import type { PlaybookHostConstructionCapabilities } from '../code.playbook/playbook-captain.js';
2
3
  export interface PlaybookSummaryPolicy {
3
4
  stateCountLabels: Readonly<Record<string, string>>;
4
5
  copyPasteGuardNames: readonly string[];
@@ -24,7 +25,7 @@ export interface BranchPlaybookRegistryEntry {
24
25
  concurrentRoleSets: readonly [];
25
26
  summaryPolicy: PlaybookSummaryPolicy;
26
27
  validateOptions(optionSlice: unknown): BranchOptions;
27
- createRuntime(options: BranchOptions, hostCapabilities: BranchPlaybookHostCapabilities): PlaybookRuntime;
28
+ createRuntime(options: BranchOptions, hostCapabilities: PlaybookHostConstructionCapabilities & PlaybookHostCapabilities): PlaybookRuntime;
28
29
  }
29
30
  export declare const branchStateCountLabels: {
30
31
  readonly createBranch: "branching round";
@@ -2,9 +2,10 @@
2
2
  // SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
3
3
 
4
4
  import createPlaybookRuntime, {
5
- type BranchPlaybookHostCapabilities,
5
+ type PlaybookHostCapabilities,
6
6
  type PlaybookRuntime,
7
7
  } from './branch.playbook.js';
8
+ import type { PlaybookHostConstructionCapabilities } from '../code.playbook/playbook-captain.js';
8
9
 
9
10
  export interface PlaybookSummaryPolicy {
10
11
  stateCountLabels: Readonly<Record<string, string>>;
@@ -33,9 +34,12 @@ export interface BranchPlaybookRegistryEntry {
33
34
  concurrentRoleSets: readonly [];
34
35
  summaryPolicy: PlaybookSummaryPolicy;
35
36
  validateOptions(optionSlice: unknown): BranchOptions;
37
+ // The linked module types live authority as opaque (link-materialization);
38
+ // the Captain entry binds its own construction capabilities here.
36
39
  createRuntime(
37
40
  options: BranchOptions,
38
- hostCapabilities: BranchPlaybookHostCapabilities,
41
+ hostCapabilities: PlaybookHostConstructionCapabilities &
42
+ PlaybookHostCapabilities,
39
43
  ): PlaybookRuntime;
40
44
  }
41
45
 
@@ -520,8 +520,6 @@ function projectionText(projection) {
520
520
  return JSON.stringify(projection);
521
521
  }
522
522
 
523
- const MISSING_CONTENT_IDENTITY = Object.freeze({ kind: 'missing' });
524
-
525
523
  function hasPath(projection, path) {
526
524
  return Object.prototype.hasOwnProperty.call(projection, path);
527
525
  }
@@ -555,9 +553,9 @@ function sameTreeEntry(left, right) {
555
553
  return left.mode === right.mode && left.oid === right.oid;
556
554
  }
557
555
 
558
- // The baseline recorded nothing at the path: a deleted worktree entry, or a
556
+ // The entry records nothing at the path: a deleted worktree entry, or a
559
557
  // staged deletion whose index holds no object.
560
- function baselineEntryIsDeletion(entry) {
558
+ function entryIsDeletion(entry) {
561
559
  return entry.worktree === undefined
562
560
  ? entry.indexMode === '000000'
563
561
  : entry.worktree.kind === 'missing';
@@ -615,14 +613,57 @@ function sameContentIdentity(left, right) {
615
613
  );
616
614
  }
617
615
 
616
+ // A worktree deletion loses the Boss's content only when neither the after
617
+ // index nor the after HEAD tree holds it; content the after HEAD cannot
618
+ // produce fails closed as lost.
619
+ async function deletionRetainsContent(baseline, after, path, entry, afterEntry) {
620
+ try {
621
+ const baselineIdentity = await baselineContentIdentity(after.worktree, entry);
622
+ if (
623
+ afterEntry.kind === 'ordinary' &&
624
+ afterEntry.indexMode !== undefined &&
625
+ afterEntry.indexMode !== '000000'
626
+ ) {
627
+ const indexIdentity = await blobContentIdentity(
628
+ after.worktree,
629
+ afterEntry.indexMode,
630
+ afterEntry.indexOid,
631
+ );
632
+ if (sameContentIdentity(baselineIdentity, indexIdentity)) return true;
633
+ }
634
+ if (baseline.head === after.head) return false;
635
+ const treeEntry = await afterTreeEntry(after.worktree, after.head, path);
636
+ if (treeEntry === undefined || treeEntry.mode === '160000') return false;
637
+ const treeIdentity = await blobContentIdentity(
638
+ after.worktree,
639
+ treeEntry.mode,
640
+ treeEntry.oid,
641
+ );
642
+ return sameContentIdentity(baselineIdentity, treeIdentity);
643
+ } catch {
644
+ return false;
645
+ }
646
+ }
647
+
618
648
  // DR-062 §1: the fate of one baseline projection entry at the end of a
619
649
  // governed call — `preserved`, `altered-uncommitted`, `altered-committed`,
620
- // `absorbed`, or `lost`.
650
+ // `absorbed`, or `lost`. Loss is decided before alteration: a kind that
651
+ // admits no content matching, a worktree deletion whose content survives in
652
+ // neither the index nor the new HEAD, and a commit that dropped the path all
653
+ // leave the Boss's content nowhere.
621
654
  async function baselineEntryFate(baseline, after, path, entry) {
622
655
  if (hasPath(after.projection, path)) {
623
- return JSON.stringify(after.projection[path]) === JSON.stringify(entry)
624
- ? 'preserved'
625
- : 'altered-uncommitted';
656
+ const afterEntry = after.projection[path];
657
+ if (JSON.stringify(afterEntry) === JSON.stringify(entry)) {
658
+ return 'preserved';
659
+ }
660
+ if (nonComparableBaselineEntry(entry)) return 'lost';
661
+ if (entryIsDeletion(afterEntry) && !entryIsDeletion(entry)) {
662
+ return (await deletionRetainsContent(baseline, after, path, entry, afterEntry))
663
+ ? 'altered-uncommitted'
664
+ : 'lost';
665
+ }
666
+ return 'altered-uncommitted';
626
667
  }
627
668
  // The entry left the working tree, so only a commit this call made can hold
628
669
  // the Boss's content; a call that moved no HEAD made none.
@@ -635,15 +676,15 @@ async function baselineEntryFate(baseline, after, path, entry) {
635
676
  // carried no change of the Boss's and his content is nowhere.
636
677
  return 'lost';
637
678
  }
638
- if (baselineEntryIsDeletion(entry)) {
679
+ if (entryIsDeletion(entry)) {
639
680
  return treeEntry === undefined ? 'absorbed' : 'lost';
640
681
  }
641
- if (treeEntry?.mode === '160000') return 'lost';
682
+ // A commit that dropped the path, or replaced it with a gitlink, carries
683
+ // no content of the Boss's to compare.
684
+ if (treeEntry === undefined || treeEntry.mode === '160000') return 'lost';
642
685
  const [baselineIdentity, treeIdentity] = await Promise.all([
643
686
  baselineContentIdentity(after.worktree, entry),
644
- treeEntry === undefined
645
- ? MISSING_CONTENT_IDENTITY
646
- : blobContentIdentity(after.worktree, treeEntry.mode, treeEntry.oid),
687
+ blobContentIdentity(after.worktree, treeEntry.mode, treeEntry.oid),
647
688
  ]);
648
689
  return sameContentIdentity(baselineIdentity, treeIdentity)
649
690
  ? 'absorbed'