@sublang/playbook 17.8.1 → 17.10.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.
@@ -13,6 +13,7 @@ import {
13
13
 
14
14
  import { assertPlaybookFailureCause } from './runtime.js';
15
15
  import type {
16
+ PlaybookBossExchange,
16
17
  CaptainResult,
17
18
  JsonValue,
18
19
  NormalizedError,
@@ -992,6 +993,20 @@ function effectInteger(value: unknown, path: string, minimum = 0): number {
992
993
  return value as number;
993
994
  }
994
995
 
996
+ // DR-090 §4: an observation's branch is evidence for the Boss only; two
997
+ // observations of one repository state are equal whatever branch each names.
998
+ function observedStatesEqual(
999
+ left: PlaybookRepositoryObservation,
1000
+ right: PlaybookRepositoryObservation,
1001
+ ): boolean {
1002
+ const { branch: _left, ...leftState } = left;
1003
+ const { branch: _right, ...rightState } = right;
1004
+ return jsonValuesEqual(
1005
+ leftState as unknown as JsonValue,
1006
+ rightState as unknown as JsonValue,
1007
+ );
1008
+ }
1009
+
995
1010
  function jsonValuesEqual(left: JsonValue, right: JsonValue): boolean {
996
1011
  if (Object.is(left, right)) return true;
997
1012
  if (Array.isArray(left) || Array.isArray(right)) {
@@ -1110,7 +1125,7 @@ function effectObservation(
1110
1125
  if (!isRecord(value)) throw new TypeError(`${path} must be an object`);
1111
1126
  rejectUnknownKeys(
1112
1127
  value,
1113
- ['worktree', 'gitDir', 'head', 'projection', 'projectionDigest'],
1128
+ ['worktree', 'gitDir', 'head', 'branch', 'projection', 'projectionDigest'],
1114
1129
  path,
1115
1130
  );
1116
1131
  requireNonEmptyString(value.worktree, `${path}.worktree`);
@@ -1119,6 +1134,10 @@ function effectObservation(
1119
1134
  if (!EFFECT_OID_PATTERN.test(head)) {
1120
1135
  throw new TypeError(`${path}.head must be a canonical Git commit OID`);
1121
1136
  }
1137
+ // DR-090 §4: the branch HEAD names, evidence for the Boss only.
1138
+ if (own(value, 'branch')) {
1139
+ requireNonEmptyString(value.branch, `${path}.branch`);
1140
+ }
1122
1141
  if (!isRecord(value.projection)) {
1123
1142
  throw new TypeError(`${path}.projection must be a path-keyed object`);
1124
1143
  }
@@ -1252,10 +1271,7 @@ function effectReceipt(
1252
1271
  if (
1253
1272
  classification === 'unchanged' &&
1254
1273
  after !== undefined &&
1255
- !jsonValuesEqual(
1256
- baseline as unknown as JsonValue,
1257
- after as unknown as JsonValue,
1258
- )
1274
+ !observedStatesEqual(baseline, after)
1259
1275
  ) {
1260
1276
  throw new TypeError(`${path} classified unchanged observations that differ`);
1261
1277
  }
@@ -1370,8 +1386,8 @@ export interface PlaybookSemanticOutcomeSpec {
1370
1386
 
1371
1387
  /**
1372
1388
  * The JSON value shape a compiled machine admits for one semantic-owned field
1373
- * (DR-089). A field the machine admits as a string, or whose shape the
1374
- * machine does not reveal, is `string`.
1389
+ * (DR-089). A field the machine admits as a string is `string`; a field whose
1390
+ * shape the machine does not reveal has none (DR-091).
1375
1391
  */
1376
1392
  export type PlaybookSemanticValueShape =
1377
1393
  | 'string'
@@ -1382,8 +1398,9 @@ export type PlaybookSemanticValueShape =
1382
1398
  | 'boolean';
1383
1399
 
1384
1400
  /**
1385
- * Per outcome, the non-string shapes of its semantic-owned fields; a field
1386
- * left out is a string.
1401
+ * Per outcome, the shapes of its semantic-owned fields; a field left out has
1402
+ * no revealed shape and takes any JSON value, the machine's own validator
1403
+ * deciding it (DR-091).
1387
1404
  */
1388
1405
  export type PlaybookSemanticValueShapes = Readonly<
1389
1406
  Record<string, Readonly<Record<string, PlaybookSemanticValueShape>>>
@@ -1444,7 +1461,8 @@ export interface PlaybookSemanticEvidenceInput {
1444
1461
  readonly runtimeFields?: unknown;
1445
1462
  /**
1446
1463
  * DR-089: the shape the compiled machine admits for each semantic-owned
1447
- * field, keyed by outcome; every field it leaves out is a string.
1464
+ * field, keyed by outcome; a field it leaves out takes any JSON value
1465
+ * (DR-091).
1448
1466
  */
1449
1467
  readonly semanticValueShapes?: PlaybookSemanticValueShapes;
1450
1468
  }
@@ -1548,14 +1566,13 @@ function snapshotSemanticCandidate(
1548
1566
  );
1549
1567
  }
1550
1568
  // DR-089: each semantic-owned field has the shape the compiled machine
1551
- // admits for it, a string unless the machine admits another.
1569
+ // admits for it. DR-091: a field whose shape the machine did not reveal
1570
+ // takes any JSON value, and the machine's own validator decides it.
1552
1571
  const outcomeShapes =
1553
1572
  shapes !== undefined && own(shapes, guard) ? shapes[guard] : undefined;
1554
1573
  for (const field of semanticFields) {
1555
- const shape =
1556
- outcomeShapes !== undefined && own(outcomeShapes, field)
1557
- ? outcomeShapes[field]!
1558
- : 'string';
1574
+ if (outcomeShapes === undefined || !own(outcomeShapes, field)) continue;
1575
+ const shape = outcomeShapes[field]!;
1559
1576
  if (!isPlaybookSemanticValueShape(detached[field], shape)) {
1560
1577
  semanticCandidateStructureError(
1561
1578
  shape === 'string'
@@ -2099,7 +2116,7 @@ function effectBoundary(value: unknown, index: number): PlaybookEffectBoundary {
2099
2116
  }
2100
2117
  if (own(value, 'restored')) {
2101
2118
  const restored = effectObservation(value.restored, `${path}.restored`);
2102
- if (!jsonValuesEqual(restored as unknown as JsonValue, baseline as unknown as JsonValue) ||
2119
+ if (!observedStatesEqual(restored, baseline) ||
2103
2120
  !isRecord(value.physicalReceipt) || after?.head !== baseline.head ||
2104
2121
  (value.dispositions as string[]).some((item) => item !== 'unchanged') ||
2105
2122
  value.cohortId !== undefined || value.logicalOperationId !== undefined) {
@@ -2237,10 +2254,7 @@ function effectLogicalOperation(
2237
2254
  if (
2238
2255
  previous.physicalReceipt === undefined ||
2239
2256
  previous.after === undefined ||
2240
- !jsonValuesEqual(
2241
- boundary.baseline as unknown as JsonValue,
2242
- previous.after as unknown as JsonValue,
2243
- )
2257
+ !observedStatesEqual(boundary.baseline, previous.after)
2244
2258
  ) {
2245
2259
  throw new TypeError(
2246
2260
  `${path}.boundaryIds must form one completed checkpoint chain`,
@@ -2259,10 +2273,7 @@ function effectLogicalOperation(
2259
2273
  );
2260
2274
  if (
2261
2275
  latestAfter === undefined ||
2262
- !jsonValuesEqual(
2263
- checkpoint as unknown as JsonValue,
2264
- latestAfter as unknown as JsonValue,
2265
- )
2276
+ !observedStatesEqual(checkpoint, latestAfter)
2266
2277
  ) {
2267
2278
  throw new TypeError(`${path}.checkpoint must equal its latest boundary after`);
2268
2279
  }
@@ -2737,6 +2748,7 @@ export function assertPlaybookRuntimeSnapshot(
2737
2748
  'failedEffectAttempt',
2738
2749
  'recoveryCheckpoint',
2739
2750
  'suspendedCall',
2751
+ 'bossExchanges',
2740
2752
  ],
2741
2753
  'runtime snapshot',
2742
2754
  );
@@ -3044,6 +3056,22 @@ export function assertPlaybookRuntimeSnapshot(
3044
3056
  }
3045
3057
  recoveryCheckpoint = Object.freeze({stateId, prompt, boundaryPrefix, machine: snapshotJsonValue(checkpoint.machine), ...(own(checkpoint, 'id') ? { id: effectUuid(checkpoint.id, 'recoveryCheckpoint.id') } : {}), ...(own(checkpoint, 'result') ? { result: snapshotJsonValue(checkpoint.result) } : {}), ...(checkpoint.delivered === true ? { delivered: true as const } : {})});
3046
3058
  }
3059
+ // DR-090 §1: the run's Boss exchanges, present only when there is one.
3060
+ let bossExchanges: readonly PlaybookBossExchange[] | undefined;
3061
+ if (own(snapshot, 'bossExchanges')) {
3062
+ const list = snapshot.bossExchanges;
3063
+ if (!Array.isArray(list) || list.length === 0) {
3064
+ throw new TypeError('runtime snapshot bossExchanges must be a nonempty list');
3065
+ }
3066
+ bossExchanges = Object.freeze(list.map((exchange: unknown, index: number) => {
3067
+ const path = `runtime snapshot bossExchanges[${index}]`;
3068
+ if (!isRecord(exchange)) throw new TypeError(`${path} must be an object`);
3069
+ rejectUnknownKeys(exchange, ['question', 'reply'], path);
3070
+ const question = requireNonEmptyString(exchange.question, `${path}.question`);
3071
+ const reply = requireNonEmptyString(exchange.reply, `${path}.reply`);
3072
+ return Object.freeze({ question, reply });
3073
+ }));
3074
+ }
3047
3075
  const fields = {
3048
3076
  playbookId,
3049
3077
  machine,
@@ -3067,6 +3095,7 @@ export function assertPlaybookRuntimeSnapshot(
3067
3095
  schemaVersion: 4,
3068
3096
  ...fields,
3069
3097
  ...(suspendedCall === undefined ? {} : { suspendedCall }),
3098
+ ...(bossExchanges === undefined ? {} : { bossExchanges }),
3070
3099
  });
3071
3100
  }
3072
3101