@sublang/slc 0.8.0 → 0.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.
Files changed (90) hide show
  1. package/README.md +43 -14
  2. package/dist/app.d.ts +2 -2
  3. package/dist/app.d.ts.map +1 -1
  4. package/dist/app.js +6 -4
  5. package/dist/app.js.map +1 -1
  6. package/dist/artifacts.d.ts +5 -1
  7. package/dist/artifacts.d.ts.map +1 -1
  8. package/dist/artifacts.js +19 -3
  9. package/dist/artifacts.js.map +1 -1
  10. package/dist/clarification.d.ts +32 -0
  11. package/dist/clarification.d.ts.map +1 -0
  12. package/dist/clarification.js +93 -0
  13. package/dist/clarification.js.map +1 -0
  14. package/dist/compiled-execution.js +1 -1
  15. package/dist/compiled-execution.js.map +1 -1
  16. package/dist/compiled-executor.d.ts.map +1 -1
  17. package/dist/compiled-executor.js +58 -22
  18. package/dist/compiled-executor.js.map +1 -1
  19. package/dist/config-file.d.ts.map +1 -1
  20. package/dist/config-file.js.map +1 -1
  21. package/dist/config.d.ts +1 -1
  22. package/dist/config.d.ts.map +1 -1
  23. package/dist/config.js +1 -1
  24. package/dist/config.js.map +1 -1
  25. package/dist/entry-module.d.ts.map +1 -1
  26. package/dist/entry-module.js +16 -7
  27. package/dist/entry-module.js.map +1 -1
  28. package/dist/entry-options.d.ts +13 -0
  29. package/dist/entry-options.d.ts.map +1 -0
  30. package/dist/entry-options.js +116 -0
  31. package/dist/entry-options.js.map +1 -0
  32. package/dist/execution.d.ts +21 -4
  33. package/dist/execution.d.ts.map +1 -1
  34. package/dist/execution.js +58 -19
  35. package/dist/execution.js.map +1 -1
  36. package/dist/index.d.ts +1 -0
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +1 -0
  39. package/dist/index.js.map +1 -1
  40. package/dist/interpreter.d.ts.map +1 -1
  41. package/dist/interpreter.js +20 -1
  42. package/dist/interpreter.js.map +1 -1
  43. package/dist/invocation.d.ts.map +1 -1
  44. package/dist/link.d.ts.map +1 -1
  45. package/dist/phase-runner.d.ts +1 -4
  46. package/dist/phase-runner.d.ts.map +1 -1
  47. package/dist/phase-runner.js +4 -1
  48. package/dist/phase-runner.js.map +1 -1
  49. package/dist/phase.d.ts.map +1 -1
  50. package/dist/phase.js.map +1 -1
  51. package/dist/pin-currency.js.map +1 -1
  52. package/dist/pin-generate.js.map +1 -1
  53. package/dist/pin-inputs.js.map +1 -1
  54. package/dist/pins.d.ts.map +1 -1
  55. package/dist/pins.js.map +1 -1
  56. package/dist/pipeline.d.ts.map +1 -1
  57. package/dist/playbook-contract.d.ts.map +1 -1
  58. package/dist/playbook-contract.js.map +1 -1
  59. package/dist/playbook-ports.d.ts +9 -1
  60. package/dist/playbook-ports.d.ts.map +1 -1
  61. package/dist/playbook-ports.js +24 -6
  62. package/dist/playbook-ports.js.map +1 -1
  63. package/dist/progress.js.map +1 -1
  64. package/dist/resolver.js.map +1 -1
  65. package/dist/reviewing-agent.d.ts +2 -2
  66. package/dist/reviewing-agent.d.ts.map +1 -1
  67. package/dist/reviewing-agent.js +34 -12
  68. package/dist/reviewing-agent.js.map +1 -1
  69. package/dist/runner.d.ts +4 -2
  70. package/dist/runner.d.ts.map +1 -1
  71. package/dist/runner.js +361 -45
  72. package/dist/runner.js.map +1 -1
  73. package/dist/slc.config.template.yaml +1 -1
  74. package/dist/verify-coverage.d.ts +7 -0
  75. package/dist/verify-coverage.d.ts.map +1 -1
  76. package/dist/verify-coverage.js +837 -251
  77. package/dist/verify-coverage.js.map +1 -1
  78. package/dist/verify-source.d.ts.map +1 -1
  79. package/dist/verify-source.js +61 -20
  80. package/dist/verify-source.js.map +1 -1
  81. package/dist/verify-support.js.map +1 -1
  82. package/dist/verify-typescript.d.ts +3 -0
  83. package/dist/verify-typescript.d.ts.map +1 -0
  84. package/dist/verify-typescript.js +87 -0
  85. package/dist/verify-typescript.js.map +1 -0
  86. package/dist/verify.d.ts +28 -10
  87. package/dist/verify.d.ts.map +1 -1
  88. package/dist/verify.js +624 -129
  89. package/dist/verify.js.map +1 -1
  90. package/package.json +17 -16
package/dist/verify.js CHANGED
@@ -109,6 +109,10 @@ export async function playbookProvenanceForLinkTarget(linkTarget) {
109
109
  cursor = parent;
110
110
  }
111
111
  }
112
+ /** Both XState initial-transition forms select one immediate child key. */
113
+ export function initialStateTarget(initial) {
114
+ return typeof initial === 'string' ? initial : initial?.target;
115
+ }
112
116
  const ITEM_HEADING = /^###\s+([A-Za-z][\w-]*)\s*$/;
113
117
  // The `text2gears` item form names a delegated player as "Captain shall prompt
114
118
  // <Player>" (or a "relay ... to <Player>" variant); English players are
@@ -132,6 +136,7 @@ const SECTION_HEADING = /^##\s/;
132
136
  const RESULTS_LABEL = /^Results:\s*$/;
133
137
  const RESULTS_LABEL_NEAR_MISS = /^Results\s*:?[ \t]*$/;
134
138
  const RESULT_BULLET = /^-\s+`([A-Za-z_$][A-Za-z0-9_$]*)`:\s+(\S(?:.*\S)?)\s*$/;
139
+ const PLAYBOOK_CALL_SEQUENCING_NEAR_MISS = /\bCaptain shall (first|then|next|finally) call playbook\b/u;
135
140
  /**
136
141
  * Parses the GEARS items from a `gears` artifact: each `### <ID>` item's player,
137
142
  * blockquoted acting prompt, and optional ordered `Results:` metadata.
@@ -203,7 +208,7 @@ export function parseGearsItems(gears) {
203
208
  }
204
209
  current = null;
205
210
  };
206
- for (const line of gears.split('\n')) {
211
+ for (const line of gears.split(/\r?\n/)) {
207
212
  const heading = ITEM_HEADING.exec(line);
208
213
  if (heading !== null) {
209
214
  flush();
@@ -266,6 +271,7 @@ export function parseGearsItems(gears) {
266
271
  if (guard === NEEDS_BOSS_REPLY) {
267
272
  current.resultFindings.push(`${NEEDS_BOSS_REPLY} is compiler-owned and shall not be source metadata`);
268
273
  }
274
+ current.resultFindings.push(...outputFieldGuidanceFindings(guard, description));
269
275
  current.results.push([guard, description]);
270
276
  continue;
271
277
  }
@@ -307,6 +313,68 @@ export function parseGearsItems(gears) {
307
313
  flush();
308
314
  return items;
309
315
  }
316
+ /** Backticks in an output clause declare fields, never nested prose (DR-051). */
317
+ function outputFieldGuidanceFindings(guard, description) {
318
+ const marker = description.indexOf('Output shall include');
319
+ if (marker < 0)
320
+ return [];
321
+ let depth = 0;
322
+ const findings = [];
323
+ for (const [token] of description.slice(marker).matchAll(/`[^`]+`|[()]/g)) {
324
+ if (token === '(')
325
+ depth++;
326
+ else if (token === ')')
327
+ depth = Math.max(0, depth - 1);
328
+ else if (depth > 0) {
329
+ findings.push(`result \`${guard}\` puts ${token} inside parenthetical output guidance; output-clause backticks declare fields, so move the field declaration outside parentheses or use plain guidance text or a complete field annotation`);
330
+ }
331
+ }
332
+ return findings;
333
+ }
334
+ function gearsResultFindings(items) {
335
+ return items.flatMap((item) => (item.resultFindings ?? []).map((finding) => `GEARS item ${item.id}: ${finding}`));
336
+ }
337
+ function gearsActorFindings(gears) {
338
+ const findings = [];
339
+ let item;
340
+ let inResults = false;
341
+ for (const line of gears.split('\n')) {
342
+ const heading = ITEM_HEADING.exec(line);
343
+ if (heading !== null) {
344
+ item = heading[1];
345
+ inResults = false;
346
+ continue;
347
+ }
348
+ if (SECTION_HEADING.test(line)) {
349
+ item = undefined;
350
+ inResults = false;
351
+ continue;
352
+ }
353
+ if (item === undefined)
354
+ continue;
355
+ if (BLOCKQUOTE.test(line))
356
+ continue;
357
+ if (RESULTS_LABEL.test(line) || RESULTS_LABEL_NEAR_MISS.test(line)) {
358
+ inResults = true;
359
+ continue;
360
+ }
361
+ if (inResults)
362
+ continue;
363
+ const nearMiss = PLAYBOOK_CALL_SEQUENCING_NEAR_MISS.exec(line);
364
+ if (nearMiss === null)
365
+ continue;
366
+ findings.push(`GEARS item ${item}: ${JSON.stringify(nearMiss[0])} is not a valid nested-playbook acting clause; use exact \`Captain shall call playbook ...:\` and preserve sequencing in When/While or continuation prose`);
367
+ }
368
+ return findings;
369
+ }
370
+ /** Existing GEARS result-parser findings, without requiring a consumer FSM. */
371
+ export function checkGearsResultContract(gears) {
372
+ return gearsResultFindings(parseGearsItems(gears));
373
+ }
374
+ /** GEARS actor-clause findings, without changing parser classification. */
375
+ export function checkGearsActorContract(gears) {
376
+ return gearsActorFindings(gears);
377
+ }
310
378
  const ROLE_DECLARATION = /^(?:#{1,6}\s+(Roles|Players)|(Roles|Players):)\s*$/;
311
379
  /** Canonical lowercase local-role id used by schema-3 artifacts. */
312
380
  export function canonicalRoleId(name) {
@@ -475,7 +543,13 @@ function normalizeInvokes(invoke) {
475
543
  }
476
544
  return typeof invoke === 'object' && invoke !== null ? [invoke] : [];
477
545
  }
478
- function invocationInput(invoke, context = {}) {
546
+ function invocationInput(invoke, context = {}, allowStatic = false) {
547
+ if (allowStatic &&
548
+ typeof invoke.input === 'object' &&
549
+ invoke.input !== null &&
550
+ !Array.isArray(invoke.input)) {
551
+ return { value: invoke.input };
552
+ }
479
553
  if (typeof invoke.input !== 'function')
480
554
  return { invalid: true };
481
555
  let value;
@@ -517,6 +591,15 @@ function metadataRole(state) {
517
591
  return undefined;
518
592
  return playbook.role;
519
593
  }
594
+ /** The published terminal kind a root final state declares, if any. */
595
+ function metadataTerminal(state) {
596
+ if (typeof state.meta !== 'object' || state.meta === null)
597
+ return undefined;
598
+ const playbook = state.meta.playbook;
599
+ if (typeof playbook !== 'object' || playbook === null)
600
+ return undefined;
601
+ return playbook.terminal;
602
+ }
520
603
  function stateIdConsistencyFindings(node, inputStateId) {
521
604
  if (!isNonEmptyString(inputStateId))
522
605
  return [];
@@ -554,6 +637,36 @@ function structuredStateIdentityFindings(nodes) {
554
637
  }
555
638
  return findings;
556
639
  }
640
+ /**
641
+ * A schema-3 machine publishes each of its own terminal outcomes as
642
+ * `success` or `failure`, so a caller reads the child's meaning from the
643
+ * state it reached rather than from its output fields. Only the machine's own
644
+ * root final states carry that kind: a final inside a parallel region merely
645
+ * stages the join and publishes no workflow outcome.
646
+ *
647
+ * Artifacts compiled before the definition declared the kind carry none at
648
+ * all, and they are retained rather than rebuilt, so the requirement engages
649
+ * once the artifact declares one — a partly declared artifact is drift.
650
+ */
651
+ function terminalKindFindings(nodes, schema3) {
652
+ if (!schema3)
653
+ return [];
654
+ const finals = nodes.filter(({ path, state }) => path.length === 1 && state.type === 'final');
655
+ if (!finals.some(({ state }) => metadataTerminal(state) !== undefined)) {
656
+ return [];
657
+ }
658
+ const findings = [];
659
+ for (const { statePath, state } of finals) {
660
+ const terminal = metadataTerminal(state);
661
+ if (terminal === undefined) {
662
+ findings.push(`FSM final state ${statePath}: state.meta.playbook.terminal is missing while other final states declare it`);
663
+ }
664
+ else if (terminal !== 'success' && terminal !== 'failure') {
665
+ findings.push(`FSM final state ${statePath}: state.meta.playbook.terminal ${JSON.stringify(terminal)} is neither "success" nor "failure"`);
666
+ }
667
+ }
668
+ return findings;
669
+ }
557
670
  function enumerateCaptainBindings(config) {
558
671
  const out = [];
559
672
  const initial = initialMachineContext(config) ?? {};
@@ -571,7 +684,7 @@ function enumerateCaptainBindings(config) {
571
684
  state: malformedCaptainState(publicStateId(node, undefined), `invoke.input threw during introspection: ${inspected.error}`, actor, node),
572
685
  node,
573
686
  invoke,
574
- inputFn: invoke.input,
687
+ inputFn: typeof invoke.input === 'function' ? invoke.input : undefined,
575
688
  pinActor: true,
576
689
  });
577
690
  }
@@ -586,7 +699,7 @@ function enumerateCaptainBindings(config) {
586
699
  : 'invoke.input is not a function', actor, node),
587
700
  node,
588
701
  invoke,
589
- inputFn: invoke.input,
702
+ inputFn: typeof invoke.input === 'function' ? invoke.input : undefined,
590
703
  pinActor: true,
591
704
  });
592
705
  }
@@ -673,7 +786,7 @@ function enumerateCaptainBindings(config) {
673
786
  },
674
787
  node,
675
788
  invoke,
676
- inputFn: invoke.input,
789
+ inputFn: typeof invoke.input === 'function' ? invoke.input : undefined,
677
790
  pinActor,
678
791
  });
679
792
  }
@@ -694,7 +807,7 @@ function enumeratePlaybookBindings(config) {
694
807
  for (const invoke of normalizeInvokes(node.state.invoke)) {
695
808
  if (invokeSource(invoke.src) !== 'playbook')
696
809
  continue;
697
- const inspected = invocationInput(invoke, initial);
810
+ const inspected = invocationInput(invoke, initial, true);
698
811
  if ('error' in inspected) {
699
812
  out.push({
700
813
  state: malformedPlaybookState(publicStateId(node, undefined), `invoke.input threw during introspection: ${inspected.error}`, node),
@@ -735,7 +848,7 @@ function enumeratePlaybookBindings(config) {
735
848
  ...initial,
736
849
  [fields.playbookIdContext]: playbookIdSentinel,
737
850
  [fields.textContext]: textSentinel,
738
- });
851
+ }, true);
739
852
  if ('error' in wired) {
740
853
  bindingFindings.push(`invoke.input threw during dynamic context introspection: ${wired.error}`);
741
854
  }
@@ -989,6 +1102,28 @@ function concurrentRoleSets(value) {
989
1102
  }
990
1103
  return sets;
991
1104
  }
1105
+ /** Reject tags that prevent the public runtime returning a pending child. */
1106
+ export function checkFsmChildSuspension(config) {
1107
+ const nodes = walkStateNodes(config);
1108
+ const busy = (tags) => typeof tags === 'string'
1109
+ ? tags === 'playbook.busy'
1110
+ : tags?.includes('playbook.busy') === true;
1111
+ return enumeratePlaybookBindings(config).flatMap(({ node, state }) => {
1112
+ const locations = [
1113
+ ...(busy(config.tags) ? ['machine root'] : []),
1114
+ ...nodes
1115
+ .filter((ancestor) => ancestor.path.length <= node.path.length &&
1116
+ ancestor.path.every((key, index) => key === node.path[index]) &&
1117
+ busy(ancestor.state.tags))
1118
+ .map((ancestor) => ancestor.statePath),
1119
+ ];
1120
+ return locations.length === 0
1121
+ ? []
1122
+ : [
1123
+ `FSM playbook state ${state.stateId}: playbook.busy on ${locations.join(', ')} prevents returning a suspended child call; remove that tag from the call state and its ancestors`,
1124
+ ];
1125
+ });
1126
+ }
992
1127
  /**
993
1128
  * Checks GEARS↔FSM conformance and returns human-readable findings (empty when
994
1129
  * conformant): every GEARS item maps to one state with the same player and the
@@ -1015,21 +1150,44 @@ export function checkGearsFsmConformance(gears, config, options = {}) {
1015
1150
  const explicitActorStates = new Set(captainBindings
1016
1151
  .filter(({ pinActor }) => pinActor)
1017
1152
  .map(({ state }) => state));
1018
- const playbookStates = enumeratePlaybookStates(config);
1153
+ const playbookBindings = enumeratePlaybookBindings(config);
1154
+ const playbookStates = playbookBindings.map(({ state }) => state);
1155
+ const initialContext = initialMachineContext(config);
1156
+ const literalChecks = new Map();
1157
+ const literalFinding = (item, state) => {
1158
+ let checked = literalChecks.get(state);
1159
+ if (checked === undefined)
1160
+ literalChecks.set(state, (checked = new Map()));
1161
+ if (!checked.has(item.prompt)) {
1162
+ const binding = playbookBindings.find((candidate) => candidate.state === state);
1163
+ checked.set(item.prompt, binding === undefined
1164
+ ? 'has no inspectable invocation'
1165
+ : literalPlaybookTextFinding(item.prompt, binding, initialContext));
1166
+ }
1167
+ return checked.get(item.prompt);
1168
+ };
1019
1169
  const scriptStates = enumerateScriptStates(config);
1020
1170
  const findings = [];
1171
+ if (typeof config.meta === 'object' &&
1172
+ config.meta !== null &&
1173
+ Object.hasOwn(config.meta, 'playbook')) {
1174
+ findings.push('FSM machine root declares meta.playbook; public playbook metadata belongs only to state nodes under states');
1175
+ }
1021
1176
  for (const { state, nearMiss } of controllerNearMisses) {
1022
1177
  const detail = nearMiss.missing.length > 0
1023
1178
  ? `missing ${JSON.stringify(nearMiss.missing[0])}`
1024
1179
  : `extra ${JSON.stringify(nearMiss.extra[0])}`;
1025
1180
  findings.push(`FSM state ${state.stateId}: controller decision contract near-miss (${detail}); the controller domain requires exactly ${CONTROLLER_ACTION_GUARDS.join(', ')}`);
1026
1181
  }
1027
- findings.push(...structuredStateIdentityFindings(walkStateNodes(config)));
1182
+ const nodes = walkStateNodes(config);
1183
+ findings.push(...checkFsmChildSuspension(config));
1184
+ findings.push(...structuredStateIdentityFindings(nodes));
1028
1185
  findings.push(...roleContract.findings);
1029
- const requiresConcurrentRoleSets = roleContract.generation === 'schema-3' ||
1186
+ const schema3 = roleContract.generation === 'schema-3' ||
1030
1187
  controller ||
1031
1188
  options.artifactSchema === 3;
1032
- if (requiresConcurrentRoleSets) {
1189
+ findings.push(...terminalKindFindings(nodes, schema3));
1190
+ if (schema3) {
1033
1191
  const actual = concurrentRoleSets(options.concurrentRoleSets);
1034
1192
  if (actual === undefined) {
1035
1193
  findings.push('schema-3 FSM exports no valid concurrentRoleSets array');
@@ -1038,9 +1196,8 @@ export function checkGearsFsmConformance(gears, config, options = {}) {
1038
1196
  findings.push(`schema-3 FSM concurrentRoleSets ${JSON.stringify(actual)} do not match GEARS groups ${JSON.stringify(roleContract.concurrentRoleSets)}`);
1039
1197
  }
1040
1198
  }
1041
- for (const item of items) {
1042
- findings.push(...(item.resultFindings ?? []).map((finding) => `GEARS item ${item.id}: ${finding}`));
1043
- }
1199
+ findings.push(...gearsResultFindings(items));
1200
+ findings.push(...gearsActorFindings(gears));
1044
1201
  for (const state of states) {
1045
1202
  findings.push(...(state.bindingFindings ?? []).map((finding) => `FSM state ${state.stateId}: ${finding}`));
1046
1203
  }
@@ -1134,6 +1291,20 @@ export function checkGearsFsmConformance(gears, config, options = {}) {
1134
1291
  addPlaybookMatch(groupedItems[index], groupedStates[index]);
1135
1292
  }
1136
1293
  }
1294
+ // Composed literal inputs have no static text signature. The optional
1295
+ // sourceItem remains optional: match otherwise unbound calls by their whole
1296
+ // observable template, never by guessed context-field names.
1297
+ for (const item of playbookItems) {
1298
+ if (item.playbookIdContext !== undefined || playbookMatchesByItem.has(item))
1299
+ continue;
1300
+ const state = playbookStates.find((candidate) => candidate.sourceItem === undefined &&
1301
+ !matchedPlaybookStates.has(candidate) &&
1302
+ candidate.playbookIdContext === undefined &&
1303
+ candidate.playbookId === item.playbookId &&
1304
+ literalFinding(item, candidate) === undefined);
1305
+ if (state !== undefined)
1306
+ addPlaybookMatch(item, state);
1307
+ }
1137
1308
  for (const item of items) {
1138
1309
  if (isPlaybookItem(item)) {
1139
1310
  const matched = playbookMatchesByItem.get(item) ?? [];
@@ -1160,8 +1331,9 @@ export function checkGearsFsmConformance(gears, config, options = {}) {
1160
1331
  if (state.playbookId !== item.playbookId) {
1161
1332
  findings.push(`${item.id}: FSM playbook "${state.playbookId ?? ''}" is not GEARS playbook "${item.playbookId ?? ''}"`);
1162
1333
  }
1163
- if (state.text !== item.prompt) {
1164
- findings.push(`${item.id}: FSM playbook text is not the GEARS prompt verbatim`);
1334
+ const textFinding = literalFinding(item, state);
1335
+ if (textFinding !== undefined) {
1336
+ findings.push(`${item.id}: FSM playbook text ${textFinding}`);
1165
1337
  }
1166
1338
  }
1167
1339
  continue;
@@ -1428,7 +1600,7 @@ export function pinIntrospection(config) {
1428
1600
  id: typeof state.id === 'string' ? state.id : null,
1429
1601
  publicStateId: metadataStateId(state) ?? null,
1430
1602
  type: typeof state.type === 'string' ? state.type : null,
1431
- initial: typeof state.initial === 'string' ? state.initial : null,
1603
+ initial: initialStateTarget(state.initial) ?? null,
1432
1604
  tags: normalizedTags(state.tags),
1433
1605
  children: Object.keys(state.states ?? {}),
1434
1606
  invokes: normalizeInvokes(state.invoke)
@@ -1441,7 +1613,7 @@ export function pinIntrospection(config) {
1441
1613
  }
1442
1614
  : undefined;
1443
1615
  return {
1444
- initial: typeof config.initial === 'string' ? config.initial : null,
1616
+ initial: initialStateTarget(config.initial) ?? null,
1445
1617
  captain,
1446
1618
  quiescent,
1447
1619
  rootOn,
@@ -1460,10 +1632,12 @@ export function pinIntrospection(config) {
1460
1632
  * linked composer replaces. The derived facts are pinned into the emitted test
1461
1633
  * so contract drift fails it (DR-009).
1462
1634
  */
1463
- /** The exact continuation preamble the link contract mandates (link.md). */
1635
+ /** The historical full continuation format, retained for schema-1 and old composers. */
1464
1636
  export const CONTINUATION_PREAMBLE = 'You previously paused this task to ask Boss a question; Boss has now replied. Continue the same task using the reply below.';
1465
1637
  export const BOSS_QUESTION_LABEL = 'Boss question:';
1466
1638
  export const BOSS_REPLY_LABEL = 'Boss reply:';
1639
+ const CURRENT_CONTINUATION_PREAMBLE = 'Continue the same task using Boss’s reply below.';
1640
+ const CURRENT_BOSS_QUESTION_LABEL = 'Your previous question:';
1467
1641
  // Direct Captain prompts cross the callCaptain boundary and therefore must
1468
1642
  // not acquire player-only routing or session-control text. Match the stable
1469
1643
  // labelled form as well as natural-language variants; occurrence deltas below
@@ -1727,13 +1901,69 @@ export function deriveSubstitutions(config, compose, actor) {
1727
1901
  }
1728
1902
  return out;
1729
1903
  }
1904
+ /** The shared input-only portion of the canonical continuation probe. */
1905
+ function probeContinuationInput(state, inputFn, initial, artifactSchema) {
1906
+ const question = sentinelFor('question');
1907
+ const reply = sentinelFor('bossReply');
1908
+ const pendingBossQuestion = {
1909
+ ...(artifactSchema === 3
1910
+ ? state.actor === 'captain'
1911
+ ? { asker: { kind: 'captain' } }
1912
+ : { asker: { kind: 'role', roleId: state.role ?? '' } }
1913
+ : { player: state.actor === 'captain' ? 'Captain' : state.player }),
1914
+ questionId: state.stateId,
1915
+ resumeStateId: state.stateId,
1916
+ sourceItem: state.sourceItem,
1917
+ question,
1918
+ };
1919
+ return {
1920
+ ...withPlaceholderValues(state, inputFn({
1921
+ context: {
1922
+ ...ordinaryTurnContext(probeContextReads(inputFn, initial), initial),
1923
+ pendingBossQuestion,
1924
+ bossReply: reply,
1925
+ pendingBossQuestions: { [state.stateId]: pendingBossQuestion },
1926
+ bossReplies: { [state.stateId]: reply },
1927
+ },
1928
+ })),
1929
+ question,
1930
+ reply,
1931
+ };
1932
+ }
1933
+ function continuationInputFinding(stateId, input, question, reply) {
1934
+ return !carriesSentinel(input, question) || !carriesSentinel(input, reply)
1935
+ ? `${stateId}: invoke.input does not carry pendingBossQuestion/bossReply for a continuation turn`
1936
+ : undefined;
1937
+ }
1938
+ /** Known-schema FSM input checks, without requiring a linked composer. */
1939
+ export function checkFsmContinuationInputs(config, artifactSchema) {
1940
+ if (isControllerMachine(config))
1941
+ return [];
1942
+ const initial = initialMachineContext(config);
1943
+ const findings = [];
1944
+ for (const { state, inputFn } of enumerateCaptainBindings(config)) {
1945
+ if (typeof inputFn !== 'function' ||
1946
+ !Object.hasOwn(state.result, NEEDS_BOSS_REPLY))
1947
+ continue;
1948
+ try {
1949
+ const { input, question, reply } = probeContinuationInput(state, inputFn, initial, artifactSchema);
1950
+ const finding = continuationInputFinding(state.stateId, input, question, reply);
1951
+ if (finding !== undefined)
1952
+ findings.push(finding);
1953
+ }
1954
+ catch (error) {
1955
+ findings.push(`${state.stateId}: invoke.input threw on a continuation turn: ${messageOf(error)}`);
1956
+ }
1957
+ }
1958
+ return findings;
1959
+ }
1730
1960
  /**
1731
1961
  * Checks the linked composer against the link contract for every captain state
1732
1962
  * (verification-5), returning findings (empty when conformant): the prompt body is
1733
1963
  * preserved modulo substituted placeholders, the adjudicator-facing Boss-reply
1734
1964
  * contract never leaks into a player prompt, no continuation appears on an
1735
- * ordinary turn, and a Boss-reply continuation turn opens with the exact
1736
- * preamble and labelled Q&A blocks before the body.
1965
+ * ordinary turn, and a Boss-reply continuation turn opens with a supported exact
1966
+ * prefix. Only an explicitly resumed schema-3 player may omit the question.
1737
1967
  */
1738
1968
  export function checkPromptComposition(opts) {
1739
1969
  const findings = [];
@@ -1762,119 +1992,87 @@ export function checkPromptComposition(opts) {
1762
1992
  continue;
1763
1993
  const reads = probeContextReads(inputFn, initial);
1764
1994
  const substituted = substitutions[state.stateId] ?? [];
1765
- let ordinary;
1995
+ const relayFindingKeys = new Set();
1996
+ const artifactSchema = schemaResolution.findings.length > 0
1997
+ ? undefined
1998
+ : (inferredArtifactSchema ??
1999
+ (state.role !== undefined ? 3 : state.player !== '' ? 1 : undefined));
2000
+ const modes = artifactSchema === 3 && state.actor === 'player'
2001
+ ? [undefined, false, true]
2002
+ : [undefined];
1766
2003
  let promptReads;
1767
- try {
1768
- const probed = withPlaceholderValues(state, inputFn({ context: ordinaryTurnContext(reads, initial) }));
1769
- promptReads = promptSentinelFields(state, reads, probed.fields, roles);
1770
- ordinary = composeForState(opts.compose, state, probed.input, roles);
1771
- if (typeof ordinary !== 'string') {
1772
- throw new Error(`${composerName} returned a non-string value`);
2004
+ for (const resuming of modes) {
2005
+ let ordinary;
2006
+ let input;
2007
+ try {
2008
+ const probed = withPlaceholderValues(state, inputFn({ context: ordinaryTurnContext(reads, initial) }));
2009
+ input = probed.input;
2010
+ promptReads = promptSentinelFields(state, reads, probed.fields, roles);
2011
+ ordinary = composeForState(opts.compose, state, probed.input, roles, resuming);
2012
+ if (typeof ordinary !== 'string') {
2013
+ throw new Error(`${composerName} returned a non-string value`);
2014
+ }
2015
+ }
2016
+ catch (error) {
2017
+ pushUnique(findings, `${state.stateId}: ${composerName} threw on an ordinary turn: ${messageOf(error)}`);
2018
+ continue;
2019
+ }
2020
+ pushUnique(findings, ...bodyFindings(state, ordinary, substituted, promptReads, 'ordinary'), ...literalPromptRelayFindings(state, ordinary, input, substituted, promptReads, opts.compose, roles, relayFindingKeys, resuming));
2021
+ pushUnique(findings, ...promptControlFindings(state, ordinary));
2022
+ // A self-hosted playbook's domain body may legitimately quote the
2023
+ // adjudicator contract or the continuation texts (it instructs a compiler
2024
+ // about them); only occurrences the composer ADDS beyond the body's own
2025
+ // are leaks.
2026
+ if (occurrences(ordinary, BOSS_QUESTION_MARKER) >
2027
+ occurrences(state.prompt, BOSS_QUESTION_MARKER)) {
2028
+ pushUnique(findings, `${state.stateId}: the adjudicator-facing ${NEEDS_BOSS_REPLY} contract leaks into the player prompt`);
2029
+ }
2030
+ if ([
2031
+ CONTINUATION_PREAMBLE,
2032
+ CURRENT_CONTINUATION_PREAMBLE,
2033
+ BOSS_QUESTION_LABEL,
2034
+ CURRENT_BOSS_QUESTION_LABEL,
2035
+ BOSS_REPLY_LABEL,
2036
+ ].some((needle) => occurrences(ordinary, needle) > occurrences(state.prompt, needle))) {
2037
+ pushUnique(findings, `${state.stateId}: continuation blocks appear on an ordinary turn`);
1773
2038
  }
1774
- }
1775
- catch (error) {
1776
- findings.push(`${state.stateId}: ${composerName} threw on an ordinary turn: ${messageOf(error)}`);
1777
- continue;
1778
- }
1779
- findings.push(...bodyFindings(state, ordinary, substituted, promptReads, 'ordinary'));
1780
- pushUnique(findings, ...promptControlFindings(state, ordinary));
1781
- // A self-hosted playbook's domain body may legitimately quote the
1782
- // adjudicator contract or the continuation texts (it instructs a compiler
1783
- // about them); only occurrences the composer ADDS beyond the body's own
1784
- // are leaks.
1785
- if (occurrences(ordinary, BOSS_QUESTION_MARKER) >
1786
- occurrences(state.prompt, BOSS_QUESTION_MARKER)) {
1787
- findings.push(`${state.stateId}: the adjudicator-facing ${NEEDS_BOSS_REPLY} contract leaks into the player prompt`);
1788
- }
1789
- if ([CONTINUATION_PREAMBLE, BOSS_QUESTION_LABEL, BOSS_REPLY_LABEL].some((needle) => occurrences(ordinary, needle) > occurrences(state.prompt, needle))) {
1790
- findings.push(`${state.stateId}: continuation blocks appear on an ordinary turn`);
1791
2039
  }
1792
2040
  // Controllers own no Boss-reply wait. A missing ordinary result is already
1793
2041
  // diagnosed by conformance, so do not fabricate a continuation contract.
1794
2042
  if (controller || !Object.hasOwn(state.result, NEEDS_BOSS_REPLY))
1795
2043
  continue;
1796
- const artifactSchema = schemaResolution.findings.length > 0
1797
- ? undefined
1798
- : (inferredArtifactSchema ??
1799
- (state.role !== undefined ? 3 : state.player !== '' ? 1 : undefined));
1800
2044
  if (artifactSchema === undefined) {
1801
2045
  findings.push(`${state.stateId}: prompt composition requires artifactSchema 1 or 3 to probe this direct-Captain continuation`);
1802
2046
  continue;
1803
2047
  }
1804
- // A Boss-reply continuation turn: the thunk carries the pending question
1805
- // and reply, and the composer opens with the exact preamble and labelled
1806
- // Q&A blocks before the domain body (gears2fsm.md, link.md).
1807
- const question = sentinelFor('question');
1808
- const reply = sentinelFor('bossReply');
1809
- const pendingBossQuestion = {
1810
- ...(artifactSchema === 3
1811
- ? state.actor === 'captain'
1812
- ? { asker: { kind: 'captain' } }
1813
- : { asker: { kind: 'role', roleId: state.role ?? '' } }
1814
- : {
1815
- player: state.actor === 'captain' ? 'Captain' : state.player,
1816
- }),
1817
- questionId: state.stateId,
1818
- resumeStateId: state.stateId,
1819
- sourceItem: state.sourceItem,
1820
- question,
1821
- };
1822
- let continuation;
1823
- let input;
1824
- try {
1825
- const probed = withPlaceholderValues(state, inputFn({
1826
- context: {
1827
- ...ordinaryTurnContext(reads, initial),
1828
- pendingBossQuestion,
1829
- bossReply: reply,
1830
- pendingBossQuestions: {
1831
- [state.stateId]: pendingBossQuestion,
1832
- },
1833
- bossReplies: { [state.stateId]: reply },
1834
- },
1835
- }));
1836
- input = probed.input;
1837
- promptReads = promptSentinelFields(state, reads, probed.fields, roles);
1838
- continuation = composeForState(opts.compose, state, input, roles);
1839
- if (typeof continuation !== 'string') {
1840
- throw new Error(`${composerName} returned a non-string value`);
2048
+ // Probe absent and explicit fresh modes separately: wrappers may forward
2049
+ // the optional argument incorrectly. Historical composers may ignore it
2050
+ // and keep their complete Q&A prefix even when a player resumes.
2051
+ for (const resuming of modes) {
2052
+ const question = sentinelFor('question');
2053
+ const reply = sentinelFor('bossReply');
2054
+ let continuation;
2055
+ let input;
2056
+ try {
2057
+ const probed = probeContinuationInput(state, inputFn, initial, artifactSchema);
2058
+ input = probed.input;
2059
+ promptReads = promptSentinelFields(state, reads, probed.fields, roles);
2060
+ continuation = composeForState(opts.compose, state, input, roles, resuming);
2061
+ if (typeof continuation !== 'string') {
2062
+ throw new Error(`${composerName} returned a non-string value`);
2063
+ }
1841
2064
  }
1842
- }
1843
- catch (error) {
1844
- findings.push(`${state.stateId}: ${composerName} threw on a continuation turn: ${messageOf(error)}`);
1845
- continue;
1846
- }
1847
- if (!carriesSentinel(input, question) || !carriesSentinel(input, reply)) {
1848
- findings.push(`${state.stateId}: invoke.input does not carry pendingBossQuestion/bossReply for a continuation turn`);
1849
- continue;
1850
- }
1851
- if (!continuation.startsWith(`${CONTINUATION_PREAMBLE}\n\n`)) {
1852
- findings.push(`${state.stateId}: a continuation turn does not open with the exact preamble`);
1853
- }
1854
- const bodyStart = bodyIndex(state, continuation, substituted, promptReads);
1855
- const questionBlock = `${BOSS_QUESTION_LABEL}\n${question}`;
1856
- const replyBlock = `${BOSS_REPLY_LABEL}\n${reply}`;
1857
- for (const [label, value] of [
1858
- [BOSS_QUESTION_LABEL, questionBlock],
1859
- [BOSS_REPLY_LABEL, replyBlock],
1860
- ]) {
1861
- // The composer must ADD the labelled block (beyond any body-carried
1862
- // occurrence), with its sentinel value immediately below the label and
1863
- // before the body.
1864
- const at = continuation.indexOf(value);
1865
- if (occurrences(continuation, value) <= occurrences(state.prompt, value)) {
1866
- findings.push(`${state.stateId}: a continuation turn lacks the "${label}" block`);
2065
+ catch (error) {
2066
+ pushUnique(findings, `${state.stateId}: ${composerName} threw on a continuation turn: ${messageOf(error)}`);
2067
+ continue;
1867
2068
  }
1868
- else if (bodyStart !== -1 && at > bodyStart) {
1869
- findings.push(`${state.stateId}: the "${label}" block appears after the domain prompt body`);
2069
+ const inputFinding = continuationInputFinding(state.stateId, input, question, reply);
2070
+ if (inputFinding !== undefined) {
2071
+ pushUnique(findings, inputFinding);
2072
+ continue;
1870
2073
  }
2074
+ pushUnique(findings, ...continuationPrefixFindings(state, continuation, substituted, promptReads, artifactSchema, resuming, question, reply), ...bodyFindings(state, continuation, substituted, promptReads, 'continuation'), ...promptControlFindings(state, continuation), ...literalPromptRelayFindings(state, continuation, input, substituted, promptReads, opts.compose, roles, relayFindingKeys, resuming));
1871
2075
  }
1872
- const exactContinuationPrefix = `${CONTINUATION_PREAMBLE}\n\n${questionBlock}\n\n${replyBlock}\n\n`;
1873
- if (!continuation.startsWith(exactContinuationPrefix)) {
1874
- findings.push(`${state.stateId}: a continuation turn does not preserve the exact ordered Boss question/reply blocks`);
1875
- }
1876
- findings.push(...bodyFindings(state, continuation, substituted, promptReads, 'continuation'));
1877
- pushUnique(findings, ...promptControlFindings(state, continuation));
1878
2076
  }
1879
2077
  return findings;
1880
2078
  }
@@ -1884,7 +2082,7 @@ function promptSentinelFields(state, reads, placeholderFields, declaredRoles) {
1884
2082
  ? fields
1885
2083
  : [...fields, ...declaredRoles.map((role) => `promptIdentity:${role}`)];
1886
2084
  }
1887
- function composeForState(compose, state, input, declaredRoles) {
2085
+ function composeForState(compose, state, input, declaredRoles, resuming) {
1888
2086
  // Schema-1 composers and the shared default composer use their second
1889
2087
  // positional argument as a placeholder-field map. A callable proxy with a
1890
2088
  // property-clean view is therefore both an invocation-scoped schema-3 lookup
@@ -1909,9 +2107,64 @@ function composeForState(compose, state, input, declaredRoles) {
1909
2107
  getOwnPropertyDescriptor: () => undefined,
1910
2108
  });
1911
2109
  return state.actor === 'player' && state.role !== undefined
1912
- ? compose(input, promptIdentity)
2110
+ ? resuming === undefined
2111
+ ? compose(input, promptIdentity)
2112
+ : compose(input, promptIdentity, resuming)
1913
2113
  : compose(input);
1914
2114
  }
2115
+ function continuationPrefixFindings(state, composed, substituted, reads, artifactSchema, resuming, question, reply) {
2116
+ const fullProfiles = [
2117
+ { preamble: CONTINUATION_PREAMBLE, questionLabel: BOSS_QUESTION_LABEL },
2118
+ ...(artifactSchema === 3
2119
+ ? [
2120
+ {
2121
+ preamble: CURRENT_CONTINUATION_PREAMBLE,
2122
+ questionLabel: CURRENT_BOSS_QUESTION_LABEL,
2123
+ },
2124
+ ]
2125
+ : []),
2126
+ ];
2127
+ const compactAllowed = artifactSchema === 3 && state.actor === 'player' && resuming === true;
2128
+ const replyBlock = `${BOSS_REPLY_LABEL}\n${reply}`;
2129
+ const prefixes = fullProfiles.map(({ preamble, questionLabel }) => `${preamble}\n\n${questionLabel}\n${question}\n\n${replyBlock}\n\n`);
2130
+ const compactPrefix = `${CURRENT_CONTINUATION_PREAMBLE}\n\n${replyBlock}\n\n`;
2131
+ const compact = compactAllowed && composed.startsWith(compactPrefix);
2132
+ if (compactAllowed)
2133
+ prefixes.push(compactPrefix);
2134
+ const findings = [];
2135
+ if (compact &&
2136
+ [BOSS_QUESTION_LABEL, CURRENT_BOSS_QUESTION_LABEL].some((label) => occurrences(composed, `${label}\n${question}`) >
2137
+ occurrences(state.prompt, `${label}\n${question}`))) {
2138
+ findings.push(`${state.stateId}: a compact continuation adds the Boss question outside its domain prompt body`);
2139
+ }
2140
+ if (!fullProfiles.some(({ preamble }) => composed.startsWith(`${preamble}\n\n`))) {
2141
+ findings.push(`${state.stateId}: a continuation turn does not open with the exact preamble`);
2142
+ }
2143
+ // Select diagnostics by the observed full format, without permitting a
2144
+ // hybrid label or using function arity/package versions as capability evidence.
2145
+ const profile = fullProfiles.find(({ preamble }) => composed.startsWith(`${preamble}\n\n`)) ?? fullProfiles[0];
2146
+ const blocks = [
2147
+ ...(!compact
2148
+ ? [[profile.questionLabel, `${profile.questionLabel}\n${question}`]]
2149
+ : []),
2150
+ [BOSS_REPLY_LABEL, replyBlock],
2151
+ ];
2152
+ const bodyStart = bodyIndex(state, composed, substituted, reads);
2153
+ for (const [label, value] of blocks) {
2154
+ // Authored domain text can itself quote the framework contract; a real
2155
+ // continuation must add its own sentinel-bearing blocks before that body.
2156
+ if (occurrences(composed, value) <= occurrences(state.prompt, value)) {
2157
+ findings.push(`${state.stateId}: a continuation turn lacks the "${label}" block`);
2158
+ }
2159
+ else if (bodyStart !== -1 && composed.indexOf(value) > bodyStart) {
2160
+ findings.push(`${state.stateId}: the "${label}" block appears after the domain prompt body`);
2161
+ }
2162
+ }
2163
+ if (!prefixes.some((prefix) => composed.startsWith(prefix))) {
2164
+ findings.push(`${state.stateId}: a continuation turn does not preserve the exact ordered Boss question/reply blocks`);
2165
+ }
2166
+ return findings;
2167
+ }
1915
2168
  function promptControlFindings(state, composed) {
1916
2169
  const findings = [];
1917
2170
  const introducesPlayerBinding = patternOccurrences(composed, PLAYER_BINDING_MARKER) >
@@ -2005,7 +2258,12 @@ function matchPromptBody(prompt, composed, reads, expectedSubstitutions) {
2005
2258
  if (attempt === null)
2006
2259
  continue;
2007
2260
  if (expectedSubstitutions !== undefined) {
2008
- return { index: start, substitutions: [...expectedSubstitutions] };
2261
+ return {
2262
+ index: start,
2263
+ end: attempt.end,
2264
+ substitutions: [...expectedSubstitutions],
2265
+ values: attempt.values,
2266
+ };
2009
2267
  }
2010
2268
  const modes = new Map();
2011
2269
  for (let gap = 0; gap < tokens.length; gap++) {
@@ -2018,10 +2276,246 @@ function matchPromptBody(prompt, composed, reads, expectedSubstitutions) {
2018
2276
  continue;
2019
2277
  }
2020
2278
  const substitutions = placeholdersIn(prompt).filter((token) => modes.get(token)?.has('sentinel') === true);
2021
- return { index: start, substitutions };
2279
+ return {
2280
+ index: start,
2281
+ end: attempt.end,
2282
+ substitutions,
2283
+ values: attempt.values,
2284
+ };
2022
2285
  }
2023
2286
  return null;
2024
2287
  }
2288
+ /** Probe observed string relays without inventing context fields or input shapes. */
2289
+ function literalPromptRelayFindings(state, composed, input, substituted, reads, compose, roles, findingKeys, resuming) {
2290
+ if (typeof input !== 'object' || input === null)
2291
+ return [];
2292
+ const match = matchPromptBody(state.prompt, composed, reads, substituted);
2293
+ if (match === null)
2294
+ return []; // Existing body diagnostics own this case.
2295
+ const tokens = [...state.prompt.matchAll(PLACEHOLDER)];
2296
+ const groups = new Map();
2297
+ for (const [field, value] of Object.entries(input)) {
2298
+ if (CONTRACT_INPUT_FIELDS.includes(field) ||
2299
+ typeof value !== 'string' ||
2300
+ roles.some((role) => value === sentinelFor(`promptIdentity:${role}`)) ||
2301
+ !match.values.some((matched, index) => matched === value && substituted.includes(tokens[index][0])) ||
2302
+ match.values.includes(JSON.stringify(value)))
2303
+ continue;
2304
+ // Equal values may be intentional aliases. Changing them together avoids
2305
+ // guessing which of several indistinguishable fields supplied the token.
2306
+ const fields = groups.get(value) ?? [];
2307
+ fields.push(field);
2308
+ groups.set(value, fields);
2309
+ }
2310
+ if (groups.size === 0)
2311
+ return [];
2312
+ const quotedGroups = new Set([...groups.keys()].filter((value) => tokens.every((token, index) => {
2313
+ if (match.values[index] !== value)
2314
+ return true;
2315
+ const start = state.prompt.lastIndexOf('\n', token.index - 1) + 1;
2316
+ const next = state.prompt.indexOf('\n', token.index);
2317
+ const line = state.prompt
2318
+ .slice(start, next < 0 ? undefined : next)
2319
+ .replace(/\r$/, '');
2320
+ return /^> [^<>\r\n]*(<[^\s<>`]{1,60}>)$/.exec(line)?.[1] === token[0];
2321
+ })));
2322
+ const findings = [];
2323
+ for (const multiline of [false, true]) {
2324
+ const values = new Map();
2325
+ for (const [index, value] of [...groups.keys()].entries()) {
2326
+ if (multiline && !quotedGroups.has(value))
2327
+ continue;
2328
+ const literal = `relay-${index}: $& $$ $\` $' ${placeholdersIn(state.prompt).join(' ')}`;
2329
+ // No empty-line policy is inferred here; all segments are nonempty.
2330
+ values.set(value, multiline ? `${literal}\nsecond-${index}\r\nthird-${index}` : literal);
2331
+ }
2332
+ if (values.size === 0)
2333
+ continue;
2334
+ const probed = { ...input };
2335
+ for (const [value, replacement] of values) {
2336
+ for (const field of groups.get(value))
2337
+ probed[field] = replacement;
2338
+ }
2339
+ let index = 0;
2340
+ const body = state.prompt.replace(PLACEHOLDER, () => {
2341
+ const value = match.values[index++];
2342
+ const replacement = values.get(value);
2343
+ return replacement === undefined
2344
+ ? value
2345
+ : multiline
2346
+ ? replacement.replace(/\n/g, '\n> ')
2347
+ : replacement;
2348
+ });
2349
+ const expected = composed.slice(0, match.index) + body + composed.slice(match.end);
2350
+ const label = multiline
2351
+ ? 'multiline quoted-relay'
2352
+ : 'single-pass literal-relay';
2353
+ const key = `${state.stateId}:${label}`;
2354
+ const prefix = `${state.stateId}: prompt composition does not preserve ${label} text`;
2355
+ try {
2356
+ const actual = composeForState(compose, state, probed, roles, resuming);
2357
+ if (actual === expected)
2358
+ continue;
2359
+ pushRelayFinding(findings, findingKeys, key, `${prefix}; probe preserves literal text and LF/CRLF separators/quote markers; ${firstDifferenceDiagnostic(expected, actual)}`);
2360
+ }
2361
+ catch {
2362
+ // One state/representation diagnostic also covers mode-dependent errors.
2363
+ pushRelayFinding(findings, findingKeys, key, prefix);
2364
+ }
2365
+ }
2366
+ return findings;
2367
+ }
2368
+ function pushRelayFinding(findings, findingKeys, key, finding) {
2369
+ if (findingKeys.has(key))
2370
+ return;
2371
+ findingKeys.add(key);
2372
+ findings.push(finding);
2373
+ }
2374
+ function firstDifferenceDiagnostic(expected, actual) {
2375
+ let offset = 0;
2376
+ const length = Math.max(expected.length, actual.length);
2377
+ while (offset < length && expected[offset] === actual[offset])
2378
+ offset++;
2379
+ return `first difference at UTF-16 offset ${offset}: expected ${boundedJsonSnippet(expected, offset)}, actual ${boundedJsonSnippet(actual, offset)}`;
2380
+ }
2381
+ function boundedJsonSnippet(value, offset) {
2382
+ if (offset >= value.length)
2383
+ return '<end>';
2384
+ return JSON.stringify(value.slice(offset, offset + 24));
2385
+ }
2386
+ /** Verify the FSM-owned composer; child inputs reach the bridge already rendered. */
2387
+ function literalPlaybookTextFinding(prompt, binding, initial) {
2388
+ const input = binding.invoke.input;
2389
+ // Only explicit quoted relay notation establishes required runtime slots.
2390
+ // Domain metavariables and inline-code examples remain literal template text.
2391
+ const requiredRelays = new Set(prompt.split(/\r?\n/).flatMap((line) => {
2392
+ const token = /^> (?:[^<>\r\n]+: )?(<[^\s<>`]{1,60}>)$/.exec(line)?.[1];
2393
+ return token === undefined ? [] : [token];
2394
+ }));
2395
+ const unresolvedRelay = (token) => `leaves quoted child-input relay ${token} unresolved`;
2396
+ if (typeof input !== 'function') {
2397
+ if (binding.state.text !== prompt)
2398
+ return 'does not preserve the complete GEARS child-input template';
2399
+ const token = requiredRelays.values().next().value;
2400
+ return token === undefined ? undefined : unresolvedRelay(token);
2401
+ }
2402
+ const base = initial ?? {};
2403
+ const fields = new Set();
2404
+ let context = { ...base };
2405
+ // Later reads can be behind a string-valued branch. Discover them without
2406
+ // overwriting an initialized object, array, boolean, or numeric context field.
2407
+ for (let pass = 0; pass < 4; pass++) {
2408
+ const before = fields.size;
2409
+ for (const field of probeContextReads(input, context)) {
2410
+ if (base[field] === undefined || typeof base[field] === 'string')
2411
+ fields.add(field);
2412
+ }
2413
+ if (fields.size > 128)
2414
+ return 'has unsupported composition (too many scalar reads)';
2415
+ context = {
2416
+ ...base,
2417
+ ...Object.fromEntries([...fields].map((field) => [field, sentinelFor(field)])),
2418
+ };
2419
+ if (fields.size === before)
2420
+ break;
2421
+ if (pass === 3)
2422
+ return 'has unsupported composition (context reads did not stabilize)';
2423
+ }
2424
+ const probe = (values) => {
2425
+ const result = invocationInput(binding.invoke, values);
2426
+ return 'value' in result &&
2427
+ result.value.playbookId === binding.state.playbookId &&
2428
+ result.value.stateId === binding.state.stateId &&
2429
+ typeof result.value.text === 'string'
2430
+ ? result.value.text
2431
+ : undefined;
2432
+ };
2433
+ const text = probe(context);
2434
+ if (text === undefined)
2435
+ return 'has unsupported composition (input evaluation or literal identity changed)';
2436
+ const matched = matchPromptBody(prompt, text, [...fields]);
2437
+ if (matched === null || matched.index !== 0 || matched.end !== text.length) {
2438
+ return 'does not preserve the complete GEARS child-input template';
2439
+ }
2440
+ const tokens = [...prompt.matchAll(PLACEHOLDER)].map((match) => match[0]);
2441
+ const mapping = new Map();
2442
+ const observed = new Map();
2443
+ for (let index = 0; index < tokens.length; index++) {
2444
+ const token = tokens[index], value = matched.values[index];
2445
+ if (observed.has(token) && observed.get(token) !== value) {
2446
+ return `uses inconsistent context substitutions for ${token}`;
2447
+ }
2448
+ observed.set(token, value);
2449
+ if (value === token) {
2450
+ if (requiredRelays.has(token))
2451
+ return unresolvedRelay(token);
2452
+ continue;
2453
+ }
2454
+ const field = [...fields].find((name) => value === sentinelFor(name));
2455
+ if (field === undefined)
2456
+ return `has unsupported composition for ${token}`;
2457
+ mapping.set(token, { field });
2458
+ }
2459
+ // A source-labelled complete relay can carry arbitrary prose. Inline slots
2460
+ // can instead be constrained identifiers (for example a commit identity),
2461
+ // so do not invent multiline identifier inputs merely because TS erased the
2462
+ // narrower domain contract. Neither decision depends on the field's name.
2463
+ const multilineFields = new Set();
2464
+ for (const line of prompt.split(/\r?\n/)) {
2465
+ const token = /^> [^<>\r\n]*(<[^\s<>`]{1,60}>)$/.exec(line)?.[1];
2466
+ const field = token === undefined ? undefined : mapping.get(token)?.field;
2467
+ if (field !== undefined)
2468
+ multilineFields.add(field);
2469
+ }
2470
+ const payloads = Object.fromEntries([...fields].map((field) => [
2471
+ field,
2472
+ `«${field}:literal» $& $$ $\x60 $' ${[...new Set(tokens)].join(' ')}` +
2473
+ (multilineFields.has(field) ? `\n\n«${field}:second»` : ''),
2474
+ ]));
2475
+ const render = (values) => {
2476
+ // Only the source-defined standalone relay form owns empty-line removal.
2477
+ const template = prompt
2478
+ .split(/(?<=\n)/)
2479
+ .filter((line) => {
2480
+ const token = /^> (<[^\s<>`]{1,60}>)(?:\r?\n)?$/.exec(line)?.[1];
2481
+ const mapped = token === undefined ? undefined : mapping.get(token);
2482
+ return mapped === undefined || values[mapped.field] !== '';
2483
+ })
2484
+ .join('');
2485
+ return template.replace(PLACEHOLDER, (token, offset) => {
2486
+ const mapped = mapping.get(token);
2487
+ if (mapped === undefined)
2488
+ return token;
2489
+ const value = values[mapped.field];
2490
+ const lineStart = template.lastIndexOf('\n', offset - 1) + 1;
2491
+ return template.startsWith('> ', lineStart)
2492
+ ? value.replace(/\r?\n/g, (separator) => `${separator}> `)
2493
+ : value;
2494
+ });
2495
+ };
2496
+ const composed = probe({ ...base, ...payloads });
2497
+ if (composed === undefined || composed !== render(payloads)) {
2498
+ return 'does not preserve literal substitutions and quoted continuation lines';
2499
+ }
2500
+ const emptyFields = new Set();
2501
+ for (const line of prompt.split(/\r?\n/)) {
2502
+ const token = /^> (<[^\s<>`]{1,60}>)$/.exec(line)?.[1];
2503
+ const field = token === undefined ? undefined : mapping.get(token)?.field;
2504
+ if (field !== undefined)
2505
+ emptyFields.add(field);
2506
+ }
2507
+ if (emptyFields.size > 0) {
2508
+ const values = {
2509
+ ...payloads,
2510
+ ...Object.fromEntries([...emptyFields].map((field) => [field, ''])),
2511
+ };
2512
+ const emptyText = probe({ ...base, ...values });
2513
+ if (emptyText === undefined || emptyText !== render(values)) {
2514
+ return 'does not preserve source-defined empty standalone relay omission';
2515
+ }
2516
+ }
2517
+ return undefined;
2518
+ }
2025
2519
  /** Findings when a composed prompt does not preserve the domain body (verification-5). */
2026
2520
  function bodyFindings(state, composed, substituted, reads, turn) {
2027
2521
  if (matchPromptBody(state.prompt, composed, reads, substituted) !== null) {
@@ -2110,7 +2604,7 @@ import { describe, expect, it } from 'vitest';
2110
2604
  import { checkGearsFsmConformance, findConcurrentRoleSets, findMachineConfig } from ${sourceString(opts.verifyModule)};
2111
2605
  import * as fsm from ${sourceString(opts.fsmModule)};
2112
2606
 
2113
- const SCHEMA_FINDINGS = ${JSON.stringify(opts.schemaFindings ?? [], null, 2)};
2607
+ const SCHEMA_FINDINGS: readonly string[] = ${JSON.stringify(opts.schemaFindings ?? [], null, 2)};
2114
2608
 
2115
2609
  describe(${sourceString(`${opts.basename}: GEARS↔FSM conformance`)}, () => {
2116
2610
  it('uses consistent artifact-schema evidence', () => {
@@ -2221,7 +2715,7 @@ import { describe, expect, it } from 'vitest';
2221
2715
  import { findMachineConfig, pinIntrospection } from ${sourceString(opts.verifyModule)};
2222
2716
  import * as fsm from ${sourceString(opts.fsmModule)};
2223
2717
 
2224
- const PINNED = ${JSON.stringify(opts.pins, null, 2)};
2718
+ const PINNED = ${JSON.stringify(opts.pins, null, 2)} as const;
2225
2719
 
2226
2720
  describe(${sourceString(`${opts.basename}: FSM introspection`)}, () => {
2227
2721
  it('matches the machine topology pinned at build time', () => {
@@ -2253,7 +2747,7 @@ export function generatePromptContractTest(opts) {
2253
2747
  const compose = `compose${label === 'Captain' ? 'Captain' : 'Player'}`;
2254
2748
  return [
2255
2749
  `
2256
- const ${constant} = ${JSON.stringify(substituted, null, 2)};
2750
+ const ${constant} = ${JSON.stringify(substituted, null, 2)} as const;
2257
2751
 
2258
2752
  const ${compose} = (
2259
2753
  playbook as unknown as {
@@ -2303,8 +2797,8 @@ import {
2303
2797
  } from ${sourceString(opts.verifyModule)};
2304
2798
  import * as fsm from ${sourceString(opts.fsmModule)};
2305
2799
  ${composerImports}
2306
- const CONTRACT = ${JSON.stringify(opts.rows, null, 2)};
2307
- const SCHEMA_FINDINGS = ${JSON.stringify(opts.schemaFindings ?? [], null, 2)};
2800
+ const CONTRACT = ${JSON.stringify(opts.rows, null, 2)} as const;
2801
+ const SCHEMA_FINDINGS: readonly string[] = ${JSON.stringify(opts.schemaFindings ?? [], null, 2)};
2308
2802
 
2309
2803
  describe(${sourceString(`${opts.basename}: prompt contract`)}, () => {
2310
2804
  it('uses consistent artifact-schema evidence', () => {
@@ -2441,7 +2935,8 @@ export function resolveArtifactSchemaForVerification(opts) {
2441
2935
  if (linkedSignal?.historicalFallback) {
2442
2936
  return { artifactSchema: 1, findings: [] };
2443
2937
  }
2444
- const hasAmbiguousCaptainContinuation = opts.config !== undefined &&
2938
+ const hasAmbiguousCaptainContinuation = opts.requireContinuationSchema !== false &&
2939
+ opts.config !== undefined &&
2445
2940
  enumerateCaptainStates(opts.config).some((state) => state.actor === 'captain' &&
2446
2941
  Object.hasOwn(state.result, NEEDS_BOSS_REPLY));
2447
2942
  return hasAmbiguousCaptainContinuation