@sublang/playbook 17.6.0 → 17.8.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.
- package/package.json +1 -1
- package/reference/sdlc/code.playbook/bin/machine-identity.js +18 -0
- package/reference/sdlc/code.playbook/bin/repository-effects.js +41 -23
- package/reference/sdlc/code.playbook/bin/session-store.js +73 -23
- package/reference/sdlc/code.playbook/host-capabilities.d.ts +2 -0
- package/reference/sdlc/code.playbook/playbook-captain.js +162 -12
- package/reference/sdlc/code.playbook/playbook-captain.ts +217 -11
- package/reference/sdlc/code.playbook/session-assets.js +7 -1
- package/reference/sdlc/code.playbook/session-store.d.ts +13 -1
- package/reference/sdlc/code.playbook/session-store.js +11 -1
- package/slc/link.md +29 -6
- package/slc/text2gears.md +5 -3
- package/src/runtime.d.ts +6 -0
- package/src/runtime.ts +6 -0
- package/src/xstate-playbook-runtime.d.ts +20 -3
- package/src/xstate-playbook-runtime.js +267 -18
- package/src/xstate-playbook-runtime.ts +375 -21
- package/src/xstate-runtime.d.ts +23 -3
- package/src/xstate-runtime.js +97 -13
- package/src/xstate-runtime.ts +159 -19
package/src/xstate-runtime.js
CHANGED
|
@@ -796,7 +796,7 @@ function assertObservationIdentity(observation, identity, path) {
|
|
|
796
796
|
function effectReceipt(value, path, expectedBaseline, expectedAfter, matchExpectedAfter = false, dispositions) {
|
|
797
797
|
if (!isRecord(value))
|
|
798
798
|
throw new TypeError(`${path} must be an object`);
|
|
799
|
-
rejectUnknownKeys(value, ['classification', 'baseline', 'after', 'commitOid', 'preExisting'], path);
|
|
799
|
+
rejectUnknownKeys(value, ['classification', 'baseline', 'after', 'commitOid', 'commits', 'preExisting'], path);
|
|
800
800
|
if (typeof value.classification !== 'string' ||
|
|
801
801
|
!EFFECT_RECEIPT_CLASSIFICATIONS.has(value.classification)) {
|
|
802
802
|
throw new TypeError(`${path}.classification is not supported`);
|
|
@@ -837,6 +837,21 @@ function effectReceipt(value, path, expectedBaseline, expectedAfter, matchExpect
|
|
|
837
837
|
else if (commitOid !== undefined) {
|
|
838
838
|
throw new TypeError(`${path}.commitOid is permitted only for one-descendant-commit`);
|
|
839
839
|
}
|
|
840
|
+
// DR-088 §1: a receipt of more than one new commit lists every one, oldest
|
|
841
|
+
// first and ending at its commit OID; one new commit keeps today's shape.
|
|
842
|
+
if (own(value, 'commits')) {
|
|
843
|
+
const commits = value.commits;
|
|
844
|
+
if (classification !== 'one-descendant-commit' ||
|
|
845
|
+
!Array.isArray(commits) ||
|
|
846
|
+
commits.length < 2 ||
|
|
847
|
+
commits.some((oid) => typeof oid !== 'string' ||
|
|
848
|
+
!EFFECT_OID_PATTERN.test(oid) ||
|
|
849
|
+
oid === baseline.head) ||
|
|
850
|
+
new Set(commits).size !== commits.length ||
|
|
851
|
+
commits.at(-1) !== commitOid) {
|
|
852
|
+
throw new TypeError(`${path}.commits must list two or more new commits ending at the commit OID of a one-descendant-commit receipt`);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
840
855
|
if (classification === 'unchanged' &&
|
|
841
856
|
after !== undefined &&
|
|
842
857
|
!jsonValuesEqual(baseline, after)) {
|
|
@@ -902,6 +917,39 @@ function effectReceipt(value, path, expectedBaseline, expectedAfter, matchExpect
|
|
|
902
917
|
}
|
|
903
918
|
return value;
|
|
904
919
|
}
|
|
920
|
+
const SEMANTIC_VALUE_SHAPE_WORDS = {
|
|
921
|
+
string: 'a JSON string',
|
|
922
|
+
'string-array': 'a JSON array of strings',
|
|
923
|
+
'number-array': 'a JSON array of numbers',
|
|
924
|
+
'string-or-number-array': 'a JSON array of strings or numbers',
|
|
925
|
+
number: 'a JSON number',
|
|
926
|
+
boolean: 'a JSON boolean',
|
|
927
|
+
};
|
|
928
|
+
/** The words a judge prompt or validation error uses for one value shape. */
|
|
929
|
+
export function describePlaybookSemanticValueShape(shape) {
|
|
930
|
+
return SEMANTIC_VALUE_SHAPE_WORDS[shape];
|
|
931
|
+
}
|
|
932
|
+
/** Whether one detached JSON value has the given semantic value shape. */
|
|
933
|
+
function isPlaybookSemanticValueShape(value, shape) {
|
|
934
|
+
const isFiniteNumber = (entry) => typeof entry === 'number' && Number.isFinite(entry);
|
|
935
|
+
switch (shape) {
|
|
936
|
+
case 'string':
|
|
937
|
+
return typeof value === 'string';
|
|
938
|
+
case 'number':
|
|
939
|
+
return isFiniteNumber(value);
|
|
940
|
+
case 'boolean':
|
|
941
|
+
return typeof value === 'boolean';
|
|
942
|
+
case 'string-array':
|
|
943
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === 'string');
|
|
944
|
+
case 'number-array':
|
|
945
|
+
return Array.isArray(value) && value.every(isFiniteNumber);
|
|
946
|
+
case 'string-or-number-array':
|
|
947
|
+
return (Array.isArray(value) &&
|
|
948
|
+
value.every((entry) => typeof entry === 'string' || isFiniteNumber(entry)));
|
|
949
|
+
default:
|
|
950
|
+
return false;
|
|
951
|
+
}
|
|
952
|
+
}
|
|
905
953
|
/**
|
|
906
954
|
* A judge candidate is structurally invalid, rather than merely awaiting or
|
|
907
955
|
* conflicting with effect evidence. Callers use this distinction to spend at
|
|
@@ -916,7 +964,7 @@ export class PlaybookSemanticCandidateStructureError extends TypeError {
|
|
|
916
964
|
function semanticCandidateStructureError(message) {
|
|
917
965
|
throw new PlaybookSemanticCandidateStructureError(message);
|
|
918
966
|
}
|
|
919
|
-
function snapshotSemanticCandidate(value, outcomes) {
|
|
967
|
+
function snapshotSemanticCandidate(value, outcomes, shapes) {
|
|
920
968
|
let detached;
|
|
921
969
|
try {
|
|
922
970
|
detached = snapshotJsonValue(value, 'semantic candidate');
|
|
@@ -948,9 +996,17 @@ function snapshotSemanticCandidate(value, outcomes) {
|
|
|
948
996
|
(missing.length === 0 ? '' : `; missing ${missing.join(', ')}`) +
|
|
949
997
|
(extra.length === 0 ? '' : `; extra or wrongly owned ${extra.join(', ')}`));
|
|
950
998
|
}
|
|
999
|
+
// DR-089: each semantic-owned field has the shape the compiled machine
|
|
1000
|
+
// admits for it, a string unless the machine admits another.
|
|
1001
|
+
const outcomeShapes = shapes !== undefined && own(shapes, guard) ? shapes[guard] : undefined;
|
|
951
1002
|
for (const field of semanticFields) {
|
|
952
|
-
|
|
953
|
-
|
|
1003
|
+
const shape = outcomeShapes !== undefined && own(outcomeShapes, field)
|
|
1004
|
+
? outcomeShapes[field]
|
|
1005
|
+
: 'string';
|
|
1006
|
+
if (!isPlaybookSemanticValueShape(detached[field], shape)) {
|
|
1007
|
+
semanticCandidateStructureError(shape === 'string'
|
|
1008
|
+
? `field ${JSON.stringify(field)} must be a string`
|
|
1009
|
+
: `field ${JSON.stringify(field)} must be ${describePlaybookSemanticValueShape(shape)}`);
|
|
954
1010
|
}
|
|
955
1011
|
}
|
|
956
1012
|
return {
|
|
@@ -1005,7 +1061,9 @@ function divergentProjectionPaths(baseline, after) {
|
|
|
1005
1061
|
* proved. The receipt states the failure; nothing here infers one.
|
|
1006
1062
|
*/
|
|
1007
1063
|
function playbookRepositoryFailureCause(required, receipt) {
|
|
1008
|
-
|
|
1064
|
+
// DR-088 §3: a receipt listing more than one new commit reads as
|
|
1065
|
+
// `multiple-commits`, so its statement names more than one commit.
|
|
1066
|
+
const observed = receipt.commits === undefined ? receipt.classification : 'multiple-commits';
|
|
1009
1067
|
const baselineHead = receipt.baseline.head;
|
|
1010
1068
|
const after = receipt.after;
|
|
1011
1069
|
const common = { required, observed, baselineHead };
|
|
@@ -1096,11 +1154,25 @@ function unresolvedSemanticEvidence(reason, evidence, cause = assertPlaybookFail
|
|
|
1096
1154
|
})) {
|
|
1097
1155
|
return Object.freeze({ status: 'unresolved', reason, cause, evidence });
|
|
1098
1156
|
}
|
|
1157
|
+
/**
|
|
1158
|
+
* DR-088 §2: whether the player's final text names one commit as a word, by
|
|
1159
|
+
* its full OID or by an abbreviation of at least seven leading hex digits.
|
|
1160
|
+
* It designates which observed commit is the work and proves nothing itself.
|
|
1161
|
+
*/
|
|
1162
|
+
function finalTextNamesCommit(finalText, oid) {
|
|
1163
|
+
for (const [word] of finalText.matchAll(/(?<![0-9A-Za-z_])[0-9A-Fa-f]{7,64}(?![0-9A-Za-z_])/g)) {
|
|
1164
|
+
if (oid.startsWith(word.toLowerCase()))
|
|
1165
|
+
return true;
|
|
1166
|
+
}
|
|
1167
|
+
return false;
|
|
1168
|
+
}
|
|
1099
1169
|
/**
|
|
1100
1170
|
* Reconcile one exact semantic candidate with host-owned effect evidence.
|
|
1101
1171
|
* Presentation prose remains opaque: it is retained verbatim and only its
|
|
1102
1172
|
* trimmed value is copied into linker-declared presentation fields. No
|
|
1103
|
-
* repository fact is inferred from that prose or from the semantic candidate
|
|
1173
|
+
* repository fact is inferred from that prose or from the semantic candidate;
|
|
1174
|
+
* the prose may only name, as the work, the commit an `unchanged` receipt
|
|
1175
|
+
* observed at HEAD (DR-088 §2).
|
|
1104
1176
|
*/
|
|
1105
1177
|
export function reconcilePlaybookSemanticEvidence(input) {
|
|
1106
1178
|
let report;
|
|
@@ -1117,7 +1189,7 @@ export function reconcilePlaybookSemanticEvidence(input) {
|
|
|
1117
1189
|
evidence: { reason: `No declared outcome matches: ${report.blocked}` },
|
|
1118
1190
|
}));
|
|
1119
1191
|
}
|
|
1120
|
-
const { candidate, outcome } = snapshotSemanticCandidate(input.semanticCandidate, input.outcomes);
|
|
1192
|
+
const { candidate, outcome } = snapshotSemanticCandidate(input.semanticCandidate, input.outcomes, input.semanticValueShapes);
|
|
1121
1193
|
const evidence = retainedSemanticEvidence(candidate, input.finalText);
|
|
1122
1194
|
const finalText = typeof input.finalText === 'string' ? input.finalText.trim() : undefined;
|
|
1123
1195
|
if (finalText === undefined || finalText.length === 0) {
|
|
@@ -1135,9 +1207,18 @@ export function reconcilePlaybookSemanticEvidence(input) {
|
|
|
1135
1207
|
return unresolvedSemanticEvidence('invalid-repository-receipt', evidence);
|
|
1136
1208
|
}
|
|
1137
1209
|
const disposition = outcome.repositoryDisposition;
|
|
1210
|
+
// DR-088 §2: a commit outcome over an unchanged receipt takes the commit
|
|
1211
|
+
// already at HEAD when the final text names it and the call declares no
|
|
1212
|
+
// outcome of its own for leaving the repository unchanged.
|
|
1213
|
+
const alreadyAtHead = disposition === 'one-descendant-commit' &&
|
|
1214
|
+
receipt.classification === 'unchanged' &&
|
|
1215
|
+
receipt.after !== undefined &&
|
|
1216
|
+
!/^0+$/.test(receipt.after.head) &&
|
|
1217
|
+
Object.values(input.outcomes).every(({ repositoryDisposition }) => repositoryDisposition !== 'unchanged') &&
|
|
1218
|
+
finalTextNamesCommit(finalText, receipt.after.head);
|
|
1138
1219
|
const dispositionMatches = (disposition === 'unchanged' && receipt.classification === 'unchanged') ||
|
|
1139
1220
|
(disposition === 'one-descendant-commit' &&
|
|
1140
|
-
receipt.classification === 'one-descendant-commit') ||
|
|
1221
|
+
(receipt.classification === 'one-descendant-commit' || alreadyAtHead)) ||
|
|
1141
1222
|
(disposition === 'deferred' &&
|
|
1142
1223
|
candidate.guard === 'needsBossReply' &&
|
|
1143
1224
|
Object.entries(input.outcomes).some(([guard, declared]) => guard !== candidate.guard &&
|
|
@@ -1182,13 +1263,16 @@ export function reconcilePlaybookSemanticEvidence(input) {
|
|
|
1182
1263
|
// DR-045: effect injection selects by the accepted arm's declared
|
|
1183
1264
|
// repository disposition, never by the field's name. A
|
|
1184
1265
|
// one-descendant-commit arm's effect fields carry the qualifying
|
|
1185
|
-
// receipt's exact new-descendant commit OID
|
|
1186
|
-
//
|
|
1187
|
-
//
|
|
1188
|
-
//
|
|
1266
|
+
// receipt's exact new-descendant commit OID, or the HEAD OID its final
|
|
1267
|
+
// text named over an unchanged receipt (DR-088 §2); an unchanged arm's
|
|
1268
|
+
// effect fields carry the matching unchanged receipt's observed HEAD
|
|
1269
|
+
// OID. A deferred arm declares no effect field, and any shape the
|
|
1270
|
+
// validated receipt cannot prove fails closed as unresolved.
|
|
1189
1271
|
value =
|
|
1190
1272
|
disposition === 'one-descendant-commit'
|
|
1191
|
-
?
|
|
1273
|
+
? alreadyAtHead
|
|
1274
|
+
? receipt.after?.head
|
|
1275
|
+
: receipt.commitOid
|
|
1192
1276
|
: disposition === 'unchanged'
|
|
1193
1277
|
? receipt.after?.head
|
|
1194
1278
|
: undefined;
|
package/src/xstate-runtime.ts
CHANGED
|
@@ -1167,7 +1167,7 @@ function effectReceipt(
|
|
|
1167
1167
|
if (!isRecord(value)) throw new TypeError(`${path} must be an object`);
|
|
1168
1168
|
rejectUnknownKeys(
|
|
1169
1169
|
value,
|
|
1170
|
-
['classification', 'baseline', 'after', 'commitOid', 'preExisting'],
|
|
1170
|
+
['classification', 'baseline', 'after', 'commitOid', 'commits', 'preExisting'],
|
|
1171
1171
|
path,
|
|
1172
1172
|
);
|
|
1173
1173
|
if (
|
|
@@ -1227,6 +1227,28 @@ function effectReceipt(
|
|
|
1227
1227
|
`${path}.commitOid is permitted only for one-descendant-commit`,
|
|
1228
1228
|
);
|
|
1229
1229
|
}
|
|
1230
|
+
// DR-088 §1: a receipt of more than one new commit lists every one, oldest
|
|
1231
|
+
// first and ending at its commit OID; one new commit keeps today's shape.
|
|
1232
|
+
if (own(value, 'commits')) {
|
|
1233
|
+
const commits = value.commits;
|
|
1234
|
+
if (
|
|
1235
|
+
classification !== 'one-descendant-commit' ||
|
|
1236
|
+
!Array.isArray(commits) ||
|
|
1237
|
+
commits.length < 2 ||
|
|
1238
|
+
commits.some(
|
|
1239
|
+
(oid) =>
|
|
1240
|
+
typeof oid !== 'string' ||
|
|
1241
|
+
!EFFECT_OID_PATTERN.test(oid) ||
|
|
1242
|
+
oid === baseline.head,
|
|
1243
|
+
) ||
|
|
1244
|
+
new Set(commits).size !== commits.length ||
|
|
1245
|
+
commits.at(-1) !== commitOid
|
|
1246
|
+
) {
|
|
1247
|
+
throw new TypeError(
|
|
1248
|
+
`${path}.commits must list two or more new commits ending at the commit OID of a one-descendant-commit receipt`,
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1230
1252
|
if (
|
|
1231
1253
|
classification === 'unchanged' &&
|
|
1232
1254
|
after !== undefined &&
|
|
@@ -1346,6 +1368,73 @@ export interface PlaybookSemanticOutcomeSpec {
|
|
|
1346
1368
|
readonly repositoryDisposition: PlaybookRepositoryDisposition;
|
|
1347
1369
|
}
|
|
1348
1370
|
|
|
1371
|
+
/**
|
|
1372
|
+
* 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`.
|
|
1375
|
+
*/
|
|
1376
|
+
export type PlaybookSemanticValueShape =
|
|
1377
|
+
| 'string'
|
|
1378
|
+
| 'string-array'
|
|
1379
|
+
| 'number-array'
|
|
1380
|
+
| 'string-or-number-array'
|
|
1381
|
+
| 'number'
|
|
1382
|
+
| 'boolean';
|
|
1383
|
+
|
|
1384
|
+
/**
|
|
1385
|
+
* Per outcome, the non-string shapes of its semantic-owned fields; a field
|
|
1386
|
+
* left out is a string.
|
|
1387
|
+
*/
|
|
1388
|
+
export type PlaybookSemanticValueShapes = Readonly<
|
|
1389
|
+
Record<string, Readonly<Record<string, PlaybookSemanticValueShape>>>
|
|
1390
|
+
>;
|
|
1391
|
+
|
|
1392
|
+
const SEMANTIC_VALUE_SHAPE_WORDS: Readonly<
|
|
1393
|
+
Record<PlaybookSemanticValueShape, string>
|
|
1394
|
+
> = {
|
|
1395
|
+
string: 'a JSON string',
|
|
1396
|
+
'string-array': 'a JSON array of strings',
|
|
1397
|
+
'number-array': 'a JSON array of numbers',
|
|
1398
|
+
'string-or-number-array': 'a JSON array of strings or numbers',
|
|
1399
|
+
number: 'a JSON number',
|
|
1400
|
+
boolean: 'a JSON boolean',
|
|
1401
|
+
};
|
|
1402
|
+
|
|
1403
|
+
/** The words a judge prompt or validation error uses for one value shape. */
|
|
1404
|
+
export function describePlaybookSemanticValueShape(
|
|
1405
|
+
shape: PlaybookSemanticValueShape,
|
|
1406
|
+
): string {
|
|
1407
|
+
return SEMANTIC_VALUE_SHAPE_WORDS[shape];
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
/** Whether one detached JSON value has the given semantic value shape. */
|
|
1411
|
+
function isPlaybookSemanticValueShape(
|
|
1412
|
+
value: unknown,
|
|
1413
|
+
shape: PlaybookSemanticValueShape,
|
|
1414
|
+
): boolean {
|
|
1415
|
+
const isFiniteNumber = (entry: unknown): boolean =>
|
|
1416
|
+
typeof entry === 'number' && Number.isFinite(entry);
|
|
1417
|
+
switch (shape) {
|
|
1418
|
+
case 'string':
|
|
1419
|
+
return typeof value === 'string';
|
|
1420
|
+
case 'number':
|
|
1421
|
+
return isFiniteNumber(value);
|
|
1422
|
+
case 'boolean':
|
|
1423
|
+
return typeof value === 'boolean';
|
|
1424
|
+
case 'string-array':
|
|
1425
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === 'string');
|
|
1426
|
+
case 'number-array':
|
|
1427
|
+
return Array.isArray(value) && value.every(isFiniteNumber);
|
|
1428
|
+
case 'string-or-number-array':
|
|
1429
|
+
return (
|
|
1430
|
+
Array.isArray(value) &&
|
|
1431
|
+
value.every((entry) => typeof entry === 'string' || isFiniteNumber(entry))
|
|
1432
|
+
);
|
|
1433
|
+
default:
|
|
1434
|
+
return false;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1349
1438
|
/** Evidence available when one governed semantic candidate is reconciled. */
|
|
1350
1439
|
export interface PlaybookSemanticEvidenceInput {
|
|
1351
1440
|
readonly outcomes: Readonly<Record<string, PlaybookSemanticOutcomeSpec>>;
|
|
@@ -1353,11 +1442,16 @@ export interface PlaybookSemanticEvidenceInput {
|
|
|
1353
1442
|
readonly finalText?: unknown;
|
|
1354
1443
|
readonly receipt?: unknown;
|
|
1355
1444
|
readonly runtimeFields?: unknown;
|
|
1445
|
+
/**
|
|
1446
|
+
* 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.
|
|
1448
|
+
*/
|
|
1449
|
+
readonly semanticValueShapes?: PlaybookSemanticValueShapes;
|
|
1356
1450
|
}
|
|
1357
1451
|
|
|
1358
1452
|
/** The exact detached actor output admitted to FSM delivery. */
|
|
1359
1453
|
export type PlaybookReconciledSemanticOutput = Readonly<
|
|
1360
|
-
Record<string,
|
|
1454
|
+
Record<string, JsonValue>
|
|
1361
1455
|
> & {
|
|
1362
1456
|
readonly guard: string;
|
|
1363
1457
|
};
|
|
@@ -1365,7 +1459,7 @@ export type PlaybookReconciledSemanticOutput = Readonly<
|
|
|
1365
1459
|
/** Retained presentation and semantic evidence, kept separate from output. */
|
|
1366
1460
|
export interface PlaybookRetainedSemanticEvidence {
|
|
1367
1461
|
readonly finalText?: string;
|
|
1368
|
-
readonly semanticCandidate: Readonly<Record<string,
|
|
1462
|
+
readonly semanticCandidate: Readonly<Record<string, JsonValue>>;
|
|
1369
1463
|
}
|
|
1370
1464
|
|
|
1371
1465
|
export type PlaybookSemanticReconciliationReason =
|
|
@@ -1412,8 +1506,9 @@ function semanticCandidateStructureError(message: string): never {
|
|
|
1412
1506
|
function snapshotSemanticCandidate(
|
|
1413
1507
|
value: unknown,
|
|
1414
1508
|
outcomes: Readonly<Record<string, PlaybookSemanticOutcomeSpec>>,
|
|
1509
|
+
shapes: PlaybookSemanticValueShapes | undefined,
|
|
1415
1510
|
): {
|
|
1416
|
-
readonly candidate: Readonly<Record<string,
|
|
1511
|
+
readonly candidate: Readonly<Record<string, JsonValue>> & {
|
|
1417
1512
|
readonly guard: string;
|
|
1418
1513
|
};
|
|
1419
1514
|
readonly outcome: PlaybookSemanticOutcomeSpec;
|
|
@@ -1452,15 +1547,25 @@ function snapshotSemanticCandidate(
|
|
|
1452
1547
|
(extra.length === 0 ? '' : `; extra or wrongly owned ${extra.join(', ')}`),
|
|
1453
1548
|
);
|
|
1454
1549
|
}
|
|
1550
|
+
// DR-089: each semantic-owned field has the shape the compiled machine
|
|
1551
|
+
// admits for it, a string unless the machine admits another.
|
|
1552
|
+
const outcomeShapes =
|
|
1553
|
+
shapes !== undefined && own(shapes, guard) ? shapes[guard] : undefined;
|
|
1455
1554
|
for (const field of semanticFields) {
|
|
1456
|
-
|
|
1555
|
+
const shape =
|
|
1556
|
+
outcomeShapes !== undefined && own(outcomeShapes, field)
|
|
1557
|
+
? outcomeShapes[field]!
|
|
1558
|
+
: 'string';
|
|
1559
|
+
if (!isPlaybookSemanticValueShape(detached[field], shape)) {
|
|
1457
1560
|
semanticCandidateStructureError(
|
|
1458
|
-
|
|
1561
|
+
shape === 'string'
|
|
1562
|
+
? `field ${JSON.stringify(field)} must be a string`
|
|
1563
|
+
: `field ${JSON.stringify(field)} must be ${describePlaybookSemanticValueShape(shape)}`,
|
|
1459
1564
|
);
|
|
1460
1565
|
}
|
|
1461
1566
|
}
|
|
1462
1567
|
return {
|
|
1463
|
-
candidate: detached as Readonly<Record<string,
|
|
1568
|
+
candidate: detached as Readonly<Record<string, JsonValue>> & {
|
|
1464
1569
|
readonly guard: string;
|
|
1465
1570
|
},
|
|
1466
1571
|
outcome,
|
|
@@ -1468,7 +1573,7 @@ function snapshotSemanticCandidate(
|
|
|
1468
1573
|
}
|
|
1469
1574
|
|
|
1470
1575
|
function retainedSemanticEvidence(
|
|
1471
|
-
candidate: Readonly<Record<string,
|
|
1576
|
+
candidate: Readonly<Record<string, JsonValue>>,
|
|
1472
1577
|
finalText: unknown,
|
|
1473
1578
|
): PlaybookRetainedSemanticEvidence {
|
|
1474
1579
|
return Object.freeze({
|
|
@@ -1544,7 +1649,10 @@ function playbookRepositoryFailureCause(
|
|
|
1544
1649
|
required: PlaybookRepositoryDisposition,
|
|
1545
1650
|
receipt: PlaybookRepositoryReceipt,
|
|
1546
1651
|
): PlaybookFailureCause {
|
|
1547
|
-
|
|
1652
|
+
// DR-088 §3: a receipt listing more than one new commit reads as
|
|
1653
|
+
// `multiple-commits`, so its statement names more than one commit.
|
|
1654
|
+
const observed =
|
|
1655
|
+
receipt.commits === undefined ? receipt.classification : 'multiple-commits';
|
|
1548
1656
|
const baselineHead = receipt.baseline.head;
|
|
1549
1657
|
const after = receipt.after;
|
|
1550
1658
|
const common = { required, observed, baselineHead };
|
|
@@ -1645,11 +1753,27 @@ function unresolvedSemanticEvidence(
|
|
|
1645
1753
|
return Object.freeze({ status: 'unresolved', reason, cause, evidence });
|
|
1646
1754
|
}
|
|
1647
1755
|
|
|
1756
|
+
/**
|
|
1757
|
+
* DR-088 §2: whether the player's final text names one commit as a word, by
|
|
1758
|
+
* its full OID or by an abbreviation of at least seven leading hex digits.
|
|
1759
|
+
* It designates which observed commit is the work and proves nothing itself.
|
|
1760
|
+
*/
|
|
1761
|
+
function finalTextNamesCommit(finalText: string, oid: string): boolean {
|
|
1762
|
+
for (const [word] of finalText.matchAll(
|
|
1763
|
+
/(?<![0-9A-Za-z_])[0-9A-Fa-f]{7,64}(?![0-9A-Za-z_])/g,
|
|
1764
|
+
)) {
|
|
1765
|
+
if (oid.startsWith(word.toLowerCase())) return true;
|
|
1766
|
+
}
|
|
1767
|
+
return false;
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1648
1770
|
/**
|
|
1649
1771
|
* Reconcile one exact semantic candidate with host-owned effect evidence.
|
|
1650
1772
|
* Presentation prose remains opaque: it is retained verbatim and only its
|
|
1651
1773
|
* trimmed value is copied into linker-declared presentation fields. No
|
|
1652
|
-
* repository fact is inferred from that prose or from the semantic candidate
|
|
1774
|
+
* repository fact is inferred from that prose or from the semantic candidate;
|
|
1775
|
+
* the prose may only name, as the work, the commit an `unchanged` receipt
|
|
1776
|
+
* observed at HEAD (DR-088 §2).
|
|
1653
1777
|
*/
|
|
1654
1778
|
export function reconcilePlaybookSemanticEvidence(
|
|
1655
1779
|
input: PlaybookSemanticEvidenceInput,
|
|
@@ -1664,7 +1788,7 @@ export function reconcilePlaybookSemanticEvidence(
|
|
|
1664
1788
|
typeof report.blocked === 'string' && report.blocked.trim().length > 0) {
|
|
1665
1789
|
return unresolvedSemanticEvidence(
|
|
1666
1790
|
'no-matching-outcome',
|
|
1667
|
-
retainedSemanticEvidence(report as Readonly<Record<string,
|
|
1791
|
+
retainedSemanticEvidence(report as Readonly<Record<string, JsonValue>>, input.finalText),
|
|
1668
1792
|
assertPlaybookFailureCause({
|
|
1669
1793
|
code: 'runtime-defect',
|
|
1670
1794
|
evidence: { reason: `No declared outcome matches: ${report.blocked}` },
|
|
@@ -1674,6 +1798,7 @@ export function reconcilePlaybookSemanticEvidence(
|
|
|
1674
1798
|
const { candidate, outcome } = snapshotSemanticCandidate(
|
|
1675
1799
|
input.semanticCandidate,
|
|
1676
1800
|
input.outcomes,
|
|
1801
|
+
input.semanticValueShapes,
|
|
1677
1802
|
);
|
|
1678
1803
|
const evidence = retainedSemanticEvidence(candidate, input.finalText);
|
|
1679
1804
|
const finalText =
|
|
@@ -1706,10 +1831,22 @@ export function reconcilePlaybookSemanticEvidence(
|
|
|
1706
1831
|
}
|
|
1707
1832
|
|
|
1708
1833
|
const disposition = outcome.repositoryDisposition;
|
|
1834
|
+
// DR-088 §2: a commit outcome over an unchanged receipt takes the commit
|
|
1835
|
+
// already at HEAD when the final text names it and the call declares no
|
|
1836
|
+
// outcome of its own for leaving the repository unchanged.
|
|
1837
|
+
const alreadyAtHead =
|
|
1838
|
+
disposition === 'one-descendant-commit' &&
|
|
1839
|
+
receipt.classification === 'unchanged' &&
|
|
1840
|
+
receipt.after !== undefined &&
|
|
1841
|
+
!/^0+$/.test(receipt.after.head) &&
|
|
1842
|
+
Object.values(input.outcomes).every(
|
|
1843
|
+
({ repositoryDisposition }) => repositoryDisposition !== 'unchanged',
|
|
1844
|
+
) &&
|
|
1845
|
+
finalTextNamesCommit(finalText, receipt.after.head);
|
|
1709
1846
|
const dispositionMatches =
|
|
1710
1847
|
(disposition === 'unchanged' && receipt.classification === 'unchanged') ||
|
|
1711
1848
|
(disposition === 'one-descendant-commit' &&
|
|
1712
|
-
receipt.classification === 'one-descendant-commit') ||
|
|
1849
|
+
(receipt.classification === 'one-descendant-commit' || alreadyAtHead)) ||
|
|
1713
1850
|
(disposition === 'deferred' &&
|
|
1714
1851
|
candidate.guard === 'needsBossReply' &&
|
|
1715
1852
|
Object.entries(input.outcomes).some(
|
|
@@ -1761,10 +1898,10 @@ export function reconcilePlaybookSemanticEvidence(
|
|
|
1761
1898
|
return unresolvedSemanticEvidence('inconsistent-runtime-evidence', evidence);
|
|
1762
1899
|
}
|
|
1763
1900
|
|
|
1764
|
-
const output: Record<string,
|
|
1901
|
+
const output: Record<string, JsonValue> = {};
|
|
1765
1902
|
defineEnumerableDataProperty(output, 'guard', candidate.guard);
|
|
1766
1903
|
for (const [field, authority] of Object.entries(outcome.fields)) {
|
|
1767
|
-
let value:
|
|
1904
|
+
let value: JsonValue | undefined;
|
|
1768
1905
|
if (authority === 'semantic') {
|
|
1769
1906
|
value = candidate[field];
|
|
1770
1907
|
} else if (authority === 'presentation') {
|
|
@@ -1773,13 +1910,16 @@ export function reconcilePlaybookSemanticEvidence(
|
|
|
1773
1910
|
// DR-045: effect injection selects by the accepted arm's declared
|
|
1774
1911
|
// repository disposition, never by the field's name. A
|
|
1775
1912
|
// one-descendant-commit arm's effect fields carry the qualifying
|
|
1776
|
-
// receipt's exact new-descendant commit OID
|
|
1777
|
-
//
|
|
1778
|
-
//
|
|
1779
|
-
//
|
|
1913
|
+
// receipt's exact new-descendant commit OID, or the HEAD OID its final
|
|
1914
|
+
// text named over an unchanged receipt (DR-088 §2); an unchanged arm's
|
|
1915
|
+
// effect fields carry the matching unchanged receipt's observed HEAD
|
|
1916
|
+
// OID. A deferred arm declares no effect field, and any shape the
|
|
1917
|
+
// validated receipt cannot prove fails closed as unresolved.
|
|
1780
1918
|
value =
|
|
1781
1919
|
disposition === 'one-descendant-commit'
|
|
1782
|
-
?
|
|
1920
|
+
? alreadyAtHead
|
|
1921
|
+
? receipt.after?.head
|
|
1922
|
+
: receipt.commitOid
|
|
1783
1923
|
: disposition === 'unchanged'
|
|
1784
1924
|
? receipt.after?.head
|
|
1785
1925
|
: undefined;
|