@sublang/playbook 3.0.0 → 4.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.
@@ -42,6 +42,48 @@ function isFsmResultFailure(error) {
42
42
  fsmResultFailures.has(error));
43
43
  }
44
44
  // ---------------------------------------------------------------------------
45
+ // DR-022: the engine's compatibility self-report. A linked thin module
46
+ // records the values current at link time in `spec.compat`; the factory
47
+ // checks that declaration against this very module — the engine instance
48
+ // that will interpret the FSM, so the check can never consult a different
49
+ // engine copy than the one executing — and fails construction on a mismatch
50
+ // instead of misbehaving deep in a session. Raising RUNTIME_ABI or removing
51
+ // a member of SUPPORTED_ARTIFACT_SCHEMAS is a breaking change (RELEASE-15).
52
+ // ---------------------------------------------------------------------------
53
+ /** The runtime ABI this engine implements (DR-022). */
54
+ export const RUNTIME_ABI = 1;
55
+ /** The linked-artifact schema versions this engine accepts (DR-022). */
56
+ export const SUPPORTED_ARTIFACT_SCHEMAS = Object.freeze([
57
+ 1,
58
+ ]);
59
+ // PBRT-50: validate a declaration against the loaded engine, schema first,
60
+ // so one clear diagnostic covers a fully skewed artifact. Absent means a
61
+ // legacy artifact emitted before the DR-022 contract; those must keep
62
+ // loading unchanged (DR-019 §4), so there is nothing to check.
63
+ function assertRuntimeCompat(compat, label) {
64
+ if (compat === undefined)
65
+ return;
66
+ if (compat === null || typeof compat !== 'object') {
67
+ throw new TypeError(`${label} spec.compat must be an object`);
68
+ }
69
+ const { artifactSchema, runtimeAbi } = compat;
70
+ if (!Number.isSafeInteger(artifactSchema)) {
71
+ throw new TypeError(`${label} spec.compat.artifactSchema must be an integer`);
72
+ }
73
+ if (!Number.isSafeInteger(runtimeAbi)) {
74
+ throw new TypeError(`${label} spec.compat.runtimeAbi must be an integer`);
75
+ }
76
+ if (!SUPPORTED_ARTIFACT_SCHEMAS.includes(artifactSchema)) {
77
+ throw new TypeError(`${label} artifact declares schema ${artifactSchema}, but this ` +
78
+ `@sublang/playbook/xstate-runtime engine supports ` +
79
+ `[${SUPPORTED_ARTIFACT_SCHEMAS.join(', ')}]`);
80
+ }
81
+ if (runtimeAbi !== RUNTIME_ABI) {
82
+ throw new TypeError(`${label} artifact declares runtime ABI ${runtimeAbi}, but this ` +
83
+ `@sublang/playbook/xstate-runtime engine implements ${RUNTIME_ABI}`);
84
+ }
85
+ }
86
+ // ---------------------------------------------------------------------------
45
87
  // Tolerant judge-JSON recovery (slc/link.md §Boss-event mapping).
46
88
  // ---------------------------------------------------------------------------
47
89
  function isPlainObject(value) {
@@ -412,7 +454,12 @@ export function createPlayerBridge(spec, ports, getActiveSignal, boundary, onCon
412
454
  // `response` and rejects a judge reply that supplies either presentation
413
455
  // field as an undeclared extra key.
414
456
  // ---------------------------------------------------------------------------
415
- function buildCaptainJudgePrompt(input, finalText) {
457
+ /**
458
+ * Default direct-Captain adjudicator prompt (DR-025). The single statement of
459
+ * the `{ guard, …structuralPayloadFields }` reply contract, shared with the
460
+ * compiled default Captain artifact so the wording cannot drift.
461
+ */
462
+ export function defaultBuildCaptainJudgePrompt(input, finalText) {
416
463
  const lines = [];
417
464
  lines.push('Adjudicate the direct Captain output for this FSM state.');
418
465
  lines.push(`State id: ${input.stateId}`);
@@ -857,6 +904,9 @@ function makeDefaultClassifyBossText(machine, entryEvent, bossEvents) {
857
904
  */
858
905
  export function createXStatePlaybookRuntime(machine, spec) {
859
906
  const label = spec.label ?? 'playbook';
907
+ // DR-022 / PBRT-50: reject an incompatible artifact declaration before any
908
+ // machine interpretation, against this loaded engine's own self-report.
909
+ assertRuntimeCompat(spec.compat, label);
860
910
  const declaredActors = collectInvokeSources(machine);
861
911
  const resumableStateIds = spec.resumableStateIds ?? resumableStateIdsFromMachine(machine);
862
912
  const resolvePlayerIdSpec = spec.resolvePlayerId;
@@ -1337,7 +1387,7 @@ export function createXStatePlaybookRuntime(machine, spec) {
1337
1387
  if (result.status !== 'ok' || !result.finalText) {
1338
1388
  throw new Error('captainActor: boundary returned an unvalidated Captain result');
1339
1389
  }
1340
- const judgePrompt = buildCaptainJudgePrompt(input, result.finalText);
1390
+ const judgePrompt = defaultBuildCaptainJudgePrompt(input, result.finalText);
1341
1391
  const raw = await boundary.callJudge('captain-output-adjudication', input.stateId, judgePrompt, active);
1342
1392
  const output = adjudicateCaptainOutput(extractFields, input, result.finalText, raw);
1343
1393
  validateBossReplyOutput(input, output, resumableStateIds);
@@ -177,18 +177,86 @@ function isFsmResultFailure(error: unknown): boolean {
177
177
  );
178
178
  }
179
179
 
180
+ // ---------------------------------------------------------------------------
181
+ // DR-022: the engine's compatibility self-report. A linked thin module
182
+ // records the values current at link time in `spec.compat`; the factory
183
+ // checks that declaration against this very module — the engine instance
184
+ // that will interpret the FSM, so the check can never consult a different
185
+ // engine copy than the one executing — and fails construction on a mismatch
186
+ // instead of misbehaving deep in a session. Raising RUNTIME_ABI or removing
187
+ // a member of SUPPORTED_ARTIFACT_SCHEMAS is a breaking change (RELEASE-15).
188
+ // ---------------------------------------------------------------------------
189
+
190
+ /** The runtime ABI this engine implements (DR-022). */
191
+ export const RUNTIME_ABI = 1;
192
+
193
+ /** The linked-artifact schema versions this engine accepts (DR-022). */
194
+ export const SUPPORTED_ARTIFACT_SCHEMAS: readonly number[] = Object.freeze([
195
+ 1,
196
+ ]);
197
+
198
+ /** A linked artifact's declared link-time compatibility values (DR-022). */
199
+ export interface XStatePlaybookRuntimeCompat {
200
+ /** The artifact schema version the linker emitted. */
201
+ artifactSchema: number;
202
+ /** The engine ABI the artifact was linked against. */
203
+ runtimeAbi: number;
204
+ }
205
+
206
+ // PBRT-50: validate a declaration against the loaded engine, schema first,
207
+ // so one clear diagnostic covers a fully skewed artifact. Absent means a
208
+ // legacy artifact emitted before the DR-022 contract; those must keep
209
+ // loading unchanged (DR-019 §4), so there is nothing to check.
210
+ function assertRuntimeCompat(
211
+ compat: XStatePlaybookRuntimeCompat | undefined,
212
+ label: string,
213
+ ): void {
214
+ if (compat === undefined) return;
215
+ if (compat === null || typeof compat !== 'object') {
216
+ throw new TypeError(`${label} spec.compat must be an object`);
217
+ }
218
+ const { artifactSchema, runtimeAbi } = compat;
219
+ if (!Number.isSafeInteger(artifactSchema)) {
220
+ throw new TypeError(
221
+ `${label} spec.compat.artifactSchema must be an integer`,
222
+ );
223
+ }
224
+ if (!Number.isSafeInteger(runtimeAbi)) {
225
+ throw new TypeError(`${label} spec.compat.runtimeAbi must be an integer`);
226
+ }
227
+ if (!SUPPORTED_ARTIFACT_SCHEMAS.includes(artifactSchema)) {
228
+ throw new TypeError(
229
+ `${label} artifact declares schema ${artifactSchema}, but this ` +
230
+ `@sublang/playbook/xstate-runtime engine supports ` +
231
+ `[${SUPPORTED_ARTIFACT_SCHEMAS.join(', ')}]`,
232
+ );
233
+ }
234
+ if (runtimeAbi !== RUNTIME_ABI) {
235
+ throw new TypeError(
236
+ `${label} artifact declares runtime ABI ${runtimeAbi}, but this ` +
237
+ `@sublang/playbook/xstate-runtime engine implements ${RUNTIME_ABI}`,
238
+ );
239
+ }
240
+ }
241
+
180
242
  // ---------------------------------------------------------------------------
181
243
  // The per-workflow spec. Every strategy member has a generic default derived
182
244
  // from the FSM artifact's own data, so a linker-emitted thin module normally
183
- // supplies only `snapshotOptions` and, where applicable, `entryEvent`, erased
184
- // Boss-event field metadata, placeholder exceptions, and transition-event
185
- // fields. Hand-maintained artifacts may override any member to preserve their
186
- // existing observable behavior exactly.
245
+ // supplies only `snapshotOptions` and, where applicable, `compat`,
246
+ // `entryEvent`, erased Boss-event field metadata, placeholder exceptions, and
247
+ // transition-event fields. Hand-maintained artifacts may override any member
248
+ // to preserve their existing observable behavior exactly.
187
249
  // ---------------------------------------------------------------------------
188
250
 
189
251
  export interface XStatePlaybookRuntimeSpec<TOptions> {
190
252
  /** Diagnostic label used in internal invariant errors. Default 'playbook'. */
191
253
  label?: string;
254
+ /**
255
+ * Link-time compatibility declaration checked at construction against the
256
+ * loaded engine's self-report (DR-022). Absent: a legacy artifact emitted
257
+ * before the contract — constructed with no compatibility check.
258
+ */
259
+ compat?: XStatePlaybookRuntimeCompat;
192
260
  /** Validate and JSON-snapshot the caller's per-run options. */
193
261
  snapshotOptions: (value: unknown) => TOptions;
194
262
  /** Derive the FSM machine input from validated options. Default: identity. */
@@ -731,8 +799,17 @@ export function createPlayerBridge(
731
799
  // field as an undeclared extra key.
732
800
  // ---------------------------------------------------------------------------
733
801
 
734
- function buildCaptainJudgePrompt(
735
- input: PlaybookCaptainInput,
802
+ /**
803
+ * Default direct-Captain adjudicator prompt (DR-025). The single statement of
804
+ * the `{ guard, …structuralPayloadFields }` reply contract, shared with the
805
+ * compiled default Captain artifact so the wording cannot drift.
806
+ */
807
+ export function defaultBuildCaptainJudgePrompt(
808
+ input: {
809
+ readonly stateId: string;
810
+ readonly sourceItem: string;
811
+ readonly result: Readonly<Record<string, string>>;
812
+ },
736
813
  finalText: string,
737
814
  ): string {
738
815
  const lines: string[] = [];
@@ -1337,6 +1414,9 @@ export function createXStatePlaybookRuntime<TOptions>(
1337
1414
  spec: XStatePlaybookRuntimeSpec<TOptions>,
1338
1415
  ): PlaybookRuntimeFactory<TOptions> {
1339
1416
  const label = spec.label ?? 'playbook';
1417
+ // DR-022 / PBRT-50: reject an incompatible artifact declaration before any
1418
+ // machine interpretation, against this loaded engine's own self-report.
1419
+ assertRuntimeCompat(spec.compat, label);
1340
1420
  const declaredActors = collectInvokeSources(machine);
1341
1421
  const resumableStateIds =
1342
1422
  spec.resumableStateIds ?? resumableStateIdsFromMachine(machine);
@@ -1971,7 +2051,7 @@ export function createXStatePlaybookRuntime<TOptions>(
1971
2051
  'captainActor: boundary returned an unvalidated Captain result',
1972
2052
  );
1973
2053
  }
1974
- const judgePrompt = buildCaptainJudgePrompt(
2054
+ const judgePrompt = defaultBuildCaptainJudgePrompt(
1975
2055
  input,
1976
2056
  result.finalText,
1977
2057
  );