@sublang/playbook 17.8.0 → 17.9.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.
@@ -681,6 +681,13 @@ function effectInteger(value, path, minimum = 0) {
681
681
  }
682
682
  return value;
683
683
  }
684
+ // DR-090 §4: an observation's branch is evidence for the Boss only; two
685
+ // observations of one repository state are equal whatever branch each names.
686
+ function observedStatesEqual(left, right) {
687
+ const { branch: _left, ...leftState } = left;
688
+ const { branch: _right, ...rightState } = right;
689
+ return jsonValuesEqual(leftState, rightState);
690
+ }
684
691
  function jsonValuesEqual(left, right) {
685
692
  if (Object.is(left, right))
686
693
  return true;
@@ -760,13 +767,17 @@ function effectPreExisting(value, path, baseline) {
760
767
  function effectObservation(value, path) {
761
768
  if (!isRecord(value))
762
769
  throw new TypeError(`${path} must be an object`);
763
- rejectUnknownKeys(value, ['worktree', 'gitDir', 'head', 'projection', 'projectionDigest'], path);
770
+ rejectUnknownKeys(value, ['worktree', 'gitDir', 'head', 'branch', 'projection', 'projectionDigest'], path);
764
771
  requireNonEmptyString(value.worktree, `${path}.worktree`);
765
772
  requireNonEmptyString(value.gitDir, `${path}.gitDir`);
766
773
  const head = requireNonEmptyString(value.head, `${path}.head`);
767
774
  if (!EFFECT_OID_PATTERN.test(head)) {
768
775
  throw new TypeError(`${path}.head must be a canonical Git commit OID`);
769
776
  }
777
+ // DR-090 §4: the branch HEAD names, evidence for the Boss only.
778
+ if (own(value, 'branch')) {
779
+ requireNonEmptyString(value.branch, `${path}.branch`);
780
+ }
770
781
  if (!isRecord(value.projection)) {
771
782
  throw new TypeError(`${path}.projection must be a path-keyed object`);
772
783
  }
@@ -854,7 +865,7 @@ function effectReceipt(value, path, expectedBaseline, expectedAfter, matchExpect
854
865
  }
855
866
  if (classification === 'unchanged' &&
856
867
  after !== undefined &&
857
- !jsonValuesEqual(baseline, after)) {
868
+ !observedStatesEqual(baseline, after)) {
858
869
  throw new TypeError(`${path} classified unchanged observations that differ`);
859
870
  }
860
871
  // DR-062 §3: a receipt records the fate of every baseline entry, so the
@@ -997,12 +1008,13 @@ function snapshotSemanticCandidate(value, outcomes, shapes) {
997
1008
  (extra.length === 0 ? '' : `; extra or wrongly owned ${extra.join(', ')}`));
998
1009
  }
999
1010
  // DR-089: each semantic-owned field has the shape the compiled machine
1000
- // admits for it, a string unless the machine admits another.
1011
+ // admits for it. DR-091: a field whose shape the machine did not reveal
1012
+ // takes any JSON value, and the machine's own validator decides it.
1001
1013
  const outcomeShapes = shapes !== undefined && own(shapes, guard) ? shapes[guard] : undefined;
1002
1014
  for (const field of semanticFields) {
1003
- const shape = outcomeShapes !== undefined && own(outcomeShapes, field)
1004
- ? outcomeShapes[field]
1005
- : 'string';
1015
+ if (outcomeShapes === undefined || !own(outcomeShapes, field))
1016
+ continue;
1017
+ const shape = outcomeShapes[field];
1006
1018
  if (!isPlaybookSemanticValueShape(detached[field], shape)) {
1007
1019
  semanticCandidateStructureError(shape === 'string'
1008
1020
  ? `field ${JSON.stringify(field)} must be a string`
@@ -1417,7 +1429,7 @@ function effectBoundary(value, index) {
1417
1429
  }
1418
1430
  if (own(value, 'restored')) {
1419
1431
  const restored = effectObservation(value.restored, `${path}.restored`);
1420
- if (!jsonValuesEqual(restored, baseline) ||
1432
+ if (!observedStatesEqual(restored, baseline) ||
1421
1433
  !isRecord(value.physicalReceipt) || after?.head !== baseline.head ||
1422
1434
  value.dispositions.some((item) => item !== 'unchanged') ||
1423
1435
  value.cohortId !== undefined || value.logicalOperationId !== undefined) {
@@ -1506,7 +1518,7 @@ function effectLogicalOperation(value, index, boundaryById) {
1506
1518
  const previous = boundaries[boundaryIndex - 1];
1507
1519
  if (previous.physicalReceipt === undefined ||
1508
1520
  previous.after === undefined ||
1509
- !jsonValuesEqual(boundary.baseline, previous.after)) {
1521
+ !observedStatesEqual(boundary.baseline, previous.after)) {
1510
1522
  throw new TypeError(`${path}.boundaryIds must form one completed checkpoint chain`);
1511
1523
  }
1512
1524
  }
@@ -1517,7 +1529,7 @@ function effectLogicalOperation(value, index, boundaryById) {
1517
1529
  if (checkpoint !== undefined) {
1518
1530
  assertObservationIdentity(checkpoint, originalBaseline, `${path}.checkpoint`);
1519
1531
  if (latestAfter === undefined ||
1520
- !jsonValuesEqual(checkpoint, latestAfter)) {
1532
+ !observedStatesEqual(checkpoint, latestAfter)) {
1521
1533
  throw new TypeError(`${path}.checkpoint must equal its latest boundary after`);
1522
1534
  }
1523
1535
  }
@@ -1810,6 +1822,7 @@ export function assertPlaybookRuntimeSnapshot(value, expectedPlaybookId, options
1810
1822
  'failedEffectAttempt',
1811
1823
  'recoveryCheckpoint',
1812
1824
  'suspendedCall',
1825
+ 'bossExchanges',
1813
1826
  ], 'runtime snapshot');
1814
1827
  let suspendedCall;
1815
1828
  if (own(snapshot, 'suspendedCall')) {
@@ -1994,6 +2007,23 @@ export function assertPlaybookRuntimeSnapshot(value, expectedPlaybookId, options
1994
2007
  }
1995
2008
  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 } : {}) });
1996
2009
  }
2010
+ // DR-090 §1: the run's Boss exchanges, present only when there is one.
2011
+ let bossExchanges;
2012
+ if (own(snapshot, 'bossExchanges')) {
2013
+ const list = snapshot.bossExchanges;
2014
+ if (!Array.isArray(list) || list.length === 0) {
2015
+ throw new TypeError('runtime snapshot bossExchanges must be a nonempty list');
2016
+ }
2017
+ bossExchanges = Object.freeze(list.map((exchange, index) => {
2018
+ const path = `runtime snapshot bossExchanges[${index}]`;
2019
+ if (!isRecord(exchange))
2020
+ throw new TypeError(`${path} must be an object`);
2021
+ rejectUnknownKeys(exchange, ['question', 'reply'], path);
2022
+ const question = requireNonEmptyString(exchange.question, `${path}.question`);
2023
+ const reply = requireNonEmptyString(exchange.reply, `${path}.reply`);
2024
+ return Object.freeze({ question, reply });
2025
+ }));
2026
+ }
1997
2027
  const fields = {
1998
2028
  playbookId,
1999
2029
  machine,
@@ -2017,6 +2047,7 @@ export function assertPlaybookRuntimeSnapshot(value, expectedPlaybookId, options
2017
2047
  schemaVersion: 4,
2018
2048
  ...fields,
2019
2049
  ...(suspendedCall === undefined ? {} : { suspendedCall }),
2050
+ ...(bossExchanges === undefined ? {} : { bossExchanges }),
2020
2051
  });
2021
2052
  }
2022
2053
  export class NestedPlaybookCallError extends Error {
@@ -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