@sublang/slc 0.9.0 → 0.11.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/README.md +44 -15
- package/dist/app.d.ts +2 -2
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +6 -4
- package/dist/app.js.map +1 -1
- package/dist/artifacts.d.ts +5 -1
- package/dist/artifacts.d.ts.map +1 -1
- package/dist/artifacts.js +19 -3
- package/dist/artifacts.js.map +1 -1
- package/dist/clarification.d.ts +32 -0
- package/dist/clarification.d.ts.map +1 -0
- package/dist/clarification.js +93 -0
- package/dist/clarification.js.map +1 -0
- package/dist/compiled-execution.js +1 -1
- package/dist/compiled-execution.js.map +1 -1
- package/dist/compiled-executor.d.ts.map +1 -1
- package/dist/compiled-executor.js +58 -22
- package/dist/compiled-executor.js.map +1 -1
- package/dist/config-file.d.ts.map +1 -1
- package/dist/config-file.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/entry-module.d.ts.map +1 -1
- package/dist/entry-module.js +16 -7
- package/dist/entry-module.js.map +1 -1
- package/dist/entry-options.d.ts +13 -0
- package/dist/entry-options.d.ts.map +1 -0
- package/dist/entry-options.js +116 -0
- package/dist/entry-options.js.map +1 -0
- package/dist/execution.d.ts +21 -4
- package/dist/execution.d.ts.map +1 -1
- package/dist/execution.js +58 -19
- package/dist/execution.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/interpreter.d.ts.map +1 -1
- package/dist/interpreter.js +20 -1
- package/dist/interpreter.js.map +1 -1
- package/dist/invocation.d.ts.map +1 -1
- package/dist/link.d.ts.map +1 -1
- package/dist/phase-runner.d.ts +1 -4
- package/dist/phase-runner.d.ts.map +1 -1
- package/dist/phase-runner.js +4 -1
- package/dist/phase-runner.js.map +1 -1
- package/dist/phase.d.ts.map +1 -1
- package/dist/phase.js.map +1 -1
- package/dist/pin-currency.js.map +1 -1
- package/dist/pin-generate.js.map +1 -1
- package/dist/pin-inputs.js.map +1 -1
- package/dist/pins.d.ts.map +1 -1
- package/dist/pins.js.map +1 -1
- package/dist/pipeline.d.ts.map +1 -1
- package/dist/playbook-contract.d.ts.map +1 -1
- package/dist/playbook-contract.js.map +1 -1
- package/dist/playbook-ports.d.ts +9 -1
- package/dist/playbook-ports.d.ts.map +1 -1
- package/dist/playbook-ports.js +24 -6
- package/dist/playbook-ports.js.map +1 -1
- package/dist/progress.js.map +1 -1
- package/dist/resolver.js.map +1 -1
- package/dist/reviewing-agent.d.ts +2 -2
- package/dist/reviewing-agent.d.ts.map +1 -1
- package/dist/reviewing-agent.js +34 -12
- package/dist/reviewing-agent.js.map +1 -1
- package/dist/runner.d.ts +4 -2
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +361 -45
- package/dist/runner.js.map +1 -1
- package/dist/slc.config.template.yaml +1 -1
- package/dist/verify-coverage.d.ts +7 -0
- package/dist/verify-coverage.d.ts.map +1 -1
- package/dist/verify-coverage.js +805 -254
- package/dist/verify-coverage.js.map +1 -1
- package/dist/verify-source.d.ts +2 -0
- package/dist/verify-source.d.ts.map +1 -1
- package/dist/verify-source.js +370 -24
- package/dist/verify-source.js.map +1 -1
- package/dist/verify-support.js.map +1 -1
- package/dist/verify-typescript.d.ts +3 -0
- package/dist/verify-typescript.d.ts.map +1 -0
- package/dist/verify-typescript.js +87 -0
- package/dist/verify-typescript.js.map +1 -0
- package/dist/verify.d.ts +28 -10
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js +604 -133
- package/dist/verify.js.map +1 -1
- 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(
|
|
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;
|
|
@@ -610,7 +684,7 @@ function enumerateCaptainBindings(config) {
|
|
|
610
684
|
state: malformedCaptainState(publicStateId(node, undefined), `invoke.input threw during introspection: ${inspected.error}`, actor, node),
|
|
611
685
|
node,
|
|
612
686
|
invoke,
|
|
613
|
-
inputFn: invoke.input,
|
|
687
|
+
inputFn: typeof invoke.input === 'function' ? invoke.input : undefined,
|
|
614
688
|
pinActor: true,
|
|
615
689
|
});
|
|
616
690
|
}
|
|
@@ -625,7 +699,7 @@ function enumerateCaptainBindings(config) {
|
|
|
625
699
|
: 'invoke.input is not a function', actor, node),
|
|
626
700
|
node,
|
|
627
701
|
invoke,
|
|
628
|
-
inputFn: invoke.input,
|
|
702
|
+
inputFn: typeof invoke.input === 'function' ? invoke.input : undefined,
|
|
629
703
|
pinActor: true,
|
|
630
704
|
});
|
|
631
705
|
}
|
|
@@ -712,7 +786,7 @@ function enumerateCaptainBindings(config) {
|
|
|
712
786
|
},
|
|
713
787
|
node,
|
|
714
788
|
invoke,
|
|
715
|
-
inputFn: invoke.input,
|
|
789
|
+
inputFn: typeof invoke.input === 'function' ? invoke.input : undefined,
|
|
716
790
|
pinActor,
|
|
717
791
|
});
|
|
718
792
|
}
|
|
@@ -733,7 +807,7 @@ function enumeratePlaybookBindings(config) {
|
|
|
733
807
|
for (const invoke of normalizeInvokes(node.state.invoke)) {
|
|
734
808
|
if (invokeSource(invoke.src) !== 'playbook')
|
|
735
809
|
continue;
|
|
736
|
-
const inspected = invocationInput(invoke, initial);
|
|
810
|
+
const inspected = invocationInput(invoke, initial, true);
|
|
737
811
|
if ('error' in inspected) {
|
|
738
812
|
out.push({
|
|
739
813
|
state: malformedPlaybookState(publicStateId(node, undefined), `invoke.input threw during introspection: ${inspected.error}`, node),
|
|
@@ -774,7 +848,7 @@ function enumeratePlaybookBindings(config) {
|
|
|
774
848
|
...initial,
|
|
775
849
|
[fields.playbookIdContext]: playbookIdSentinel,
|
|
776
850
|
[fields.textContext]: textSentinel,
|
|
777
|
-
});
|
|
851
|
+
}, true);
|
|
778
852
|
if ('error' in wired) {
|
|
779
853
|
bindingFindings.push(`invoke.input threw during dynamic context introspection: ${wired.error}`);
|
|
780
854
|
}
|
|
@@ -1028,6 +1102,28 @@ function concurrentRoleSets(value) {
|
|
|
1028
1102
|
}
|
|
1029
1103
|
return sets;
|
|
1030
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
|
+
}
|
|
1031
1127
|
/**
|
|
1032
1128
|
* Checks GEARS↔FSM conformance and returns human-readable findings (empty when
|
|
1033
1129
|
* conformant): every GEARS item maps to one state with the same player and the
|
|
@@ -1054,9 +1150,29 @@ export function checkGearsFsmConformance(gears, config, options = {}) {
|
|
|
1054
1150
|
const explicitActorStates = new Set(captainBindings
|
|
1055
1151
|
.filter(({ pinActor }) => pinActor)
|
|
1056
1152
|
.map(({ state }) => state));
|
|
1057
|
-
const
|
|
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
|
+
};
|
|
1058
1169
|
const scriptStates = enumerateScriptStates(config);
|
|
1059
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
|
+
}
|
|
1060
1176
|
for (const { state, nearMiss } of controllerNearMisses) {
|
|
1061
1177
|
const detail = nearMiss.missing.length > 0
|
|
1062
1178
|
? `missing ${JSON.stringify(nearMiss.missing[0])}`
|
|
@@ -1064,6 +1180,7 @@ export function checkGearsFsmConformance(gears, config, options = {}) {
|
|
|
1064
1180
|
findings.push(`FSM state ${state.stateId}: controller decision contract near-miss (${detail}); the controller domain requires exactly ${CONTROLLER_ACTION_GUARDS.join(', ')}`);
|
|
1065
1181
|
}
|
|
1066
1182
|
const nodes = walkStateNodes(config);
|
|
1183
|
+
findings.push(...checkFsmChildSuspension(config));
|
|
1067
1184
|
findings.push(...structuredStateIdentityFindings(nodes));
|
|
1068
1185
|
findings.push(...roleContract.findings);
|
|
1069
1186
|
const schema3 = roleContract.generation === 'schema-3' ||
|
|
@@ -1079,9 +1196,8 @@ export function checkGearsFsmConformance(gears, config, options = {}) {
|
|
|
1079
1196
|
findings.push(`schema-3 FSM concurrentRoleSets ${JSON.stringify(actual)} do not match GEARS groups ${JSON.stringify(roleContract.concurrentRoleSets)}`);
|
|
1080
1197
|
}
|
|
1081
1198
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
}
|
|
1199
|
+
findings.push(...gearsResultFindings(items));
|
|
1200
|
+
findings.push(...gearsActorFindings(gears));
|
|
1085
1201
|
for (const state of states) {
|
|
1086
1202
|
findings.push(...(state.bindingFindings ?? []).map((finding) => `FSM state ${state.stateId}: ${finding}`));
|
|
1087
1203
|
}
|
|
@@ -1175,6 +1291,20 @@ export function checkGearsFsmConformance(gears, config, options = {}) {
|
|
|
1175
1291
|
addPlaybookMatch(groupedItems[index], groupedStates[index]);
|
|
1176
1292
|
}
|
|
1177
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
|
+
}
|
|
1178
1308
|
for (const item of items) {
|
|
1179
1309
|
if (isPlaybookItem(item)) {
|
|
1180
1310
|
const matched = playbookMatchesByItem.get(item) ?? [];
|
|
@@ -1201,8 +1331,9 @@ export function checkGearsFsmConformance(gears, config, options = {}) {
|
|
|
1201
1331
|
if (state.playbookId !== item.playbookId) {
|
|
1202
1332
|
findings.push(`${item.id}: FSM playbook "${state.playbookId ?? ''}" is not GEARS playbook "${item.playbookId ?? ''}"`);
|
|
1203
1333
|
}
|
|
1204
|
-
|
|
1205
|
-
|
|
1334
|
+
const textFinding = literalFinding(item, state);
|
|
1335
|
+
if (textFinding !== undefined) {
|
|
1336
|
+
findings.push(`${item.id}: FSM playbook text ${textFinding}`);
|
|
1206
1337
|
}
|
|
1207
1338
|
}
|
|
1208
1339
|
continue;
|
|
@@ -1469,7 +1600,7 @@ export function pinIntrospection(config) {
|
|
|
1469
1600
|
id: typeof state.id === 'string' ? state.id : null,
|
|
1470
1601
|
publicStateId: metadataStateId(state) ?? null,
|
|
1471
1602
|
type: typeof state.type === 'string' ? state.type : null,
|
|
1472
|
-
initial:
|
|
1603
|
+
initial: initialStateTarget(state.initial) ?? null,
|
|
1473
1604
|
tags: normalizedTags(state.tags),
|
|
1474
1605
|
children: Object.keys(state.states ?? {}),
|
|
1475
1606
|
invokes: normalizeInvokes(state.invoke)
|
|
@@ -1482,7 +1613,7 @@ export function pinIntrospection(config) {
|
|
|
1482
1613
|
}
|
|
1483
1614
|
: undefined;
|
|
1484
1615
|
return {
|
|
1485
|
-
initial:
|
|
1616
|
+
initial: initialStateTarget(config.initial) ?? null,
|
|
1486
1617
|
captain,
|
|
1487
1618
|
quiescent,
|
|
1488
1619
|
rootOn,
|
|
@@ -1501,10 +1632,12 @@ export function pinIntrospection(config) {
|
|
|
1501
1632
|
* linked composer replaces. The derived facts are pinned into the emitted test
|
|
1502
1633
|
* so contract drift fails it (DR-009).
|
|
1503
1634
|
*/
|
|
1504
|
-
/** The
|
|
1635
|
+
/** The historical full continuation format, retained for schema-1 and old composers. */
|
|
1505
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.';
|
|
1506
1637
|
export const BOSS_QUESTION_LABEL = 'Boss question:';
|
|
1507
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:';
|
|
1508
1641
|
// Direct Captain prompts cross the callCaptain boundary and therefore must
|
|
1509
1642
|
// not acquire player-only routing or session-control text. Match the stable
|
|
1510
1643
|
// labelled form as well as natural-language variants; occurrence deltas below
|
|
@@ -1552,16 +1685,28 @@ initial = {}) {
|
|
|
1552
1685
|
function sentinelContext(reads) {
|
|
1553
1686
|
return Object.fromEntries(reads.map((field) => [field, sentinelFor(field)]));
|
|
1554
1687
|
}
|
|
1555
|
-
// The gears2fsm-normative Boss-reply context fields:
|
|
1556
|
-
// continuation turn
|
|
1688
|
+
// The gears2fsm-normative Boss-reply context fields: filled only on a
|
|
1689
|
+
// continuation turn. An ordinary turn holds each at its initial shape — the
|
|
1690
|
+
// scalar form absent, the keyed form of a parallel machine an empty record —
|
|
1691
|
+
// so a state's input can index its own entry without meeting a shape the
|
|
1692
|
+
// machine's context type never admits (verification-5).
|
|
1557
1693
|
const BOSS_CONTEXT_FIELDS = [
|
|
1558
1694
|
'pendingBossQuestion',
|
|
1559
1695
|
'bossReply',
|
|
1560
1696
|
'pendingBossQuestions',
|
|
1561
1697
|
'bossReplies',
|
|
1562
1698
|
];
|
|
1699
|
+
const KEYED_BOSS_CONTEXT_FIELDS = ['pendingBossQuestions', 'bossReplies'];
|
|
1700
|
+
function isKeyedBossRecord(value) {
|
|
1701
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
1702
|
+
}
|
|
1563
1703
|
function ordinaryContext(reads) {
|
|
1564
|
-
return
|
|
1704
|
+
return {
|
|
1705
|
+
...sentinelContext(reads.filter((field) => !BOSS_CONTEXT_FIELDS.includes(field))),
|
|
1706
|
+
...Object.fromEntries(reads
|
|
1707
|
+
.filter((field) => KEYED_BOSS_CONTEXT_FIELDS.includes(field))
|
|
1708
|
+
.map((field) => [field, {}])),
|
|
1709
|
+
};
|
|
1565
1710
|
}
|
|
1566
1711
|
/**
|
|
1567
1712
|
* The machine's resolved initial context (verification-5): a literal record, or
|
|
@@ -1597,8 +1742,9 @@ function initialMachineContext(config) {
|
|
|
1597
1742
|
/**
|
|
1598
1743
|
* The context an ordinary-turn probe drives an `invoke.input` thunk with: the
|
|
1599
1744
|
* machine's initial context, overlaid with one string sentinel per traced read
|
|
1600
|
-
* whose initial value is absent or itself a string,
|
|
1601
|
-
* field
|
|
1745
|
+
* whose initial value is absent or itself a string, with every Boss-reply
|
|
1746
|
+
* field at its initial shape — absent, or an empty keyed record
|
|
1747
|
+
* (verification-5).
|
|
1602
1748
|
*/
|
|
1603
1749
|
function ordinaryTurnContext(reads, initial,
|
|
1604
1750
|
/** Traces the Boss-reply fields too, for wiring capture rather than a turn. */
|
|
@@ -1608,8 +1754,12 @@ includeBossFields = false) {
|
|
|
1608
1754
|
}
|
|
1609
1755
|
const context = { ...initial };
|
|
1610
1756
|
if (!includeBossFields) {
|
|
1611
|
-
for (const field of BOSS_CONTEXT_FIELDS)
|
|
1612
|
-
|
|
1757
|
+
for (const field of BOSS_CONTEXT_FIELDS) {
|
|
1758
|
+
if (isKeyedBossRecord(context[field]))
|
|
1759
|
+
context[field] = {};
|
|
1760
|
+
else
|
|
1761
|
+
delete context[field];
|
|
1762
|
+
}
|
|
1613
1763
|
}
|
|
1614
1764
|
for (const field of reads) {
|
|
1615
1765
|
if (!includeBossFields && BOSS_CONTEXT_FIELDS.includes(field))
|
|
@@ -1768,13 +1918,69 @@ export function deriveSubstitutions(config, compose, actor) {
|
|
|
1768
1918
|
}
|
|
1769
1919
|
return out;
|
|
1770
1920
|
}
|
|
1921
|
+
/** The shared input-only portion of the canonical continuation probe. */
|
|
1922
|
+
function probeContinuationInput(state, inputFn, initial, artifactSchema) {
|
|
1923
|
+
const question = sentinelFor('question');
|
|
1924
|
+
const reply = sentinelFor('bossReply');
|
|
1925
|
+
const pendingBossQuestion = {
|
|
1926
|
+
...(artifactSchema === 3
|
|
1927
|
+
? state.actor === 'captain'
|
|
1928
|
+
? { asker: { kind: 'captain' } }
|
|
1929
|
+
: { asker: { kind: 'role', roleId: state.role ?? '' } }
|
|
1930
|
+
: { player: state.actor === 'captain' ? 'Captain' : state.player }),
|
|
1931
|
+
questionId: state.stateId,
|
|
1932
|
+
resumeStateId: state.stateId,
|
|
1933
|
+
sourceItem: state.sourceItem,
|
|
1934
|
+
question,
|
|
1935
|
+
};
|
|
1936
|
+
return {
|
|
1937
|
+
...withPlaceholderValues(state, inputFn({
|
|
1938
|
+
context: {
|
|
1939
|
+
...ordinaryTurnContext(probeContextReads(inputFn, initial), initial),
|
|
1940
|
+
pendingBossQuestion,
|
|
1941
|
+
bossReply: reply,
|
|
1942
|
+
pendingBossQuestions: { [state.stateId]: pendingBossQuestion },
|
|
1943
|
+
bossReplies: { [state.stateId]: reply },
|
|
1944
|
+
},
|
|
1945
|
+
})),
|
|
1946
|
+
question,
|
|
1947
|
+
reply,
|
|
1948
|
+
};
|
|
1949
|
+
}
|
|
1950
|
+
function continuationInputFinding(stateId, input, question, reply) {
|
|
1951
|
+
return !carriesSentinel(input, question) || !carriesSentinel(input, reply)
|
|
1952
|
+
? `${stateId}: invoke.input does not carry pendingBossQuestion/bossReply for a continuation turn`
|
|
1953
|
+
: undefined;
|
|
1954
|
+
}
|
|
1955
|
+
/** Known-schema FSM input checks, without requiring a linked composer. */
|
|
1956
|
+
export function checkFsmContinuationInputs(config, artifactSchema) {
|
|
1957
|
+
if (isControllerMachine(config))
|
|
1958
|
+
return [];
|
|
1959
|
+
const initial = initialMachineContext(config);
|
|
1960
|
+
const findings = [];
|
|
1961
|
+
for (const { state, inputFn } of enumerateCaptainBindings(config)) {
|
|
1962
|
+
if (typeof inputFn !== 'function' ||
|
|
1963
|
+
!Object.hasOwn(state.result, NEEDS_BOSS_REPLY))
|
|
1964
|
+
continue;
|
|
1965
|
+
try {
|
|
1966
|
+
const { input, question, reply } = probeContinuationInput(state, inputFn, initial, artifactSchema);
|
|
1967
|
+
const finding = continuationInputFinding(state.stateId, input, question, reply);
|
|
1968
|
+
if (finding !== undefined)
|
|
1969
|
+
findings.push(finding);
|
|
1970
|
+
}
|
|
1971
|
+
catch (error) {
|
|
1972
|
+
findings.push(`${state.stateId}: invoke.input threw on a continuation turn: ${messageOf(error)}`);
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
return findings;
|
|
1976
|
+
}
|
|
1771
1977
|
/**
|
|
1772
1978
|
* Checks the linked composer against the link contract for every captain state
|
|
1773
1979
|
* (verification-5), returning findings (empty when conformant): the prompt body is
|
|
1774
1980
|
* preserved modulo substituted placeholders, the adjudicator-facing Boss-reply
|
|
1775
1981
|
* contract never leaks into a player prompt, no continuation appears on an
|
|
1776
|
-
* ordinary turn, and a Boss-reply continuation turn opens with
|
|
1777
|
-
*
|
|
1982
|
+
* ordinary turn, and a Boss-reply continuation turn opens with a supported exact
|
|
1983
|
+
* prefix. Only an explicitly resumed schema-3 player may omit the question.
|
|
1778
1984
|
*/
|
|
1779
1985
|
export function checkPromptComposition(opts) {
|
|
1780
1986
|
const findings = [];
|
|
@@ -1803,119 +2009,87 @@ export function checkPromptComposition(opts) {
|
|
|
1803
2009
|
continue;
|
|
1804
2010
|
const reads = probeContextReads(inputFn, initial);
|
|
1805
2011
|
const substituted = substitutions[state.stateId] ?? [];
|
|
1806
|
-
|
|
2012
|
+
const relayFindingKeys = new Set();
|
|
2013
|
+
const artifactSchema = schemaResolution.findings.length > 0
|
|
2014
|
+
? undefined
|
|
2015
|
+
: (inferredArtifactSchema ??
|
|
2016
|
+
(state.role !== undefined ? 3 : state.player !== '' ? 1 : undefined));
|
|
2017
|
+
const modes = artifactSchema === 3 && state.actor === 'player'
|
|
2018
|
+
? [undefined, false, true]
|
|
2019
|
+
: [undefined];
|
|
1807
2020
|
let promptReads;
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
2021
|
+
for (const resuming of modes) {
|
|
2022
|
+
let ordinary;
|
|
2023
|
+
let input;
|
|
2024
|
+
try {
|
|
2025
|
+
const probed = withPlaceholderValues(state, inputFn({ context: ordinaryTurnContext(reads, initial) }));
|
|
2026
|
+
input = probed.input;
|
|
2027
|
+
promptReads = promptSentinelFields(state, reads, probed.fields, roles);
|
|
2028
|
+
ordinary = composeForState(opts.compose, state, probed.input, roles, resuming);
|
|
2029
|
+
if (typeof ordinary !== 'string') {
|
|
2030
|
+
throw new Error(`${composerName} returned a non-string value`);
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
catch (error) {
|
|
2034
|
+
pushUnique(findings, `${state.stateId}: ${composerName} threw on an ordinary turn: ${messageOf(error)}`);
|
|
2035
|
+
continue;
|
|
2036
|
+
}
|
|
2037
|
+
pushUnique(findings, ...bodyFindings(state, ordinary, substituted, promptReads, 'ordinary'), ...literalPromptRelayFindings(state, ordinary, input, substituted, promptReads, opts.compose, roles, relayFindingKeys, resuming));
|
|
2038
|
+
pushUnique(findings, ...promptControlFindings(state, ordinary));
|
|
2039
|
+
// A self-hosted playbook's domain body may legitimately quote the
|
|
2040
|
+
// adjudicator contract or the continuation texts (it instructs a compiler
|
|
2041
|
+
// about them); only occurrences the composer ADDS beyond the body's own
|
|
2042
|
+
// are leaks.
|
|
2043
|
+
if (occurrences(ordinary, BOSS_QUESTION_MARKER) >
|
|
2044
|
+
occurrences(state.prompt, BOSS_QUESTION_MARKER)) {
|
|
2045
|
+
pushUnique(findings, `${state.stateId}: the adjudicator-facing ${NEEDS_BOSS_REPLY} contract leaks into the player prompt`);
|
|
2046
|
+
}
|
|
2047
|
+
if ([
|
|
2048
|
+
CONTINUATION_PREAMBLE,
|
|
2049
|
+
CURRENT_CONTINUATION_PREAMBLE,
|
|
2050
|
+
BOSS_QUESTION_LABEL,
|
|
2051
|
+
CURRENT_BOSS_QUESTION_LABEL,
|
|
2052
|
+
BOSS_REPLY_LABEL,
|
|
2053
|
+
].some((needle) => occurrences(ordinary, needle) > occurrences(state.prompt, needle))) {
|
|
2054
|
+
pushUnique(findings, `${state.stateId}: continuation blocks appear on an ordinary turn`);
|
|
1814
2055
|
}
|
|
1815
|
-
}
|
|
1816
|
-
catch (error) {
|
|
1817
|
-
findings.push(`${state.stateId}: ${composerName} threw on an ordinary turn: ${messageOf(error)}`);
|
|
1818
|
-
continue;
|
|
1819
|
-
}
|
|
1820
|
-
findings.push(...bodyFindings(state, ordinary, substituted, promptReads, 'ordinary'));
|
|
1821
|
-
pushUnique(findings, ...promptControlFindings(state, ordinary));
|
|
1822
|
-
// A self-hosted playbook's domain body may legitimately quote the
|
|
1823
|
-
// adjudicator contract or the continuation texts (it instructs a compiler
|
|
1824
|
-
// about them); only occurrences the composer ADDS beyond the body's own
|
|
1825
|
-
// are leaks.
|
|
1826
|
-
if (occurrences(ordinary, BOSS_QUESTION_MARKER) >
|
|
1827
|
-
occurrences(state.prompt, BOSS_QUESTION_MARKER)) {
|
|
1828
|
-
findings.push(`${state.stateId}: the adjudicator-facing ${NEEDS_BOSS_REPLY} contract leaks into the player prompt`);
|
|
1829
|
-
}
|
|
1830
|
-
if ([CONTINUATION_PREAMBLE, BOSS_QUESTION_LABEL, BOSS_REPLY_LABEL].some((needle) => occurrences(ordinary, needle) > occurrences(state.prompt, needle))) {
|
|
1831
|
-
findings.push(`${state.stateId}: continuation blocks appear on an ordinary turn`);
|
|
1832
2056
|
}
|
|
1833
2057
|
// Controllers own no Boss-reply wait. A missing ordinary result is already
|
|
1834
2058
|
// diagnosed by conformance, so do not fabricate a continuation contract.
|
|
1835
2059
|
if (controller || !Object.hasOwn(state.result, NEEDS_BOSS_REPLY))
|
|
1836
2060
|
continue;
|
|
1837
|
-
const artifactSchema = schemaResolution.findings.length > 0
|
|
1838
|
-
? undefined
|
|
1839
|
-
: (inferredArtifactSchema ??
|
|
1840
|
-
(state.role !== undefined ? 3 : state.player !== '' ? 1 : undefined));
|
|
1841
2061
|
if (artifactSchema === undefined) {
|
|
1842
2062
|
findings.push(`${state.stateId}: prompt composition requires artifactSchema 1 or 3 to probe this direct-Captain continuation`);
|
|
1843
2063
|
continue;
|
|
1844
2064
|
}
|
|
1845
|
-
//
|
|
1846
|
-
//
|
|
1847
|
-
// Q&A
|
|
1848
|
-
const
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
question,
|
|
1862
|
-
};
|
|
1863
|
-
let continuation;
|
|
1864
|
-
let input;
|
|
1865
|
-
try {
|
|
1866
|
-
const probed = withPlaceholderValues(state, inputFn({
|
|
1867
|
-
context: {
|
|
1868
|
-
...ordinaryTurnContext(reads, initial),
|
|
1869
|
-
pendingBossQuestion,
|
|
1870
|
-
bossReply: reply,
|
|
1871
|
-
pendingBossQuestions: {
|
|
1872
|
-
[state.stateId]: pendingBossQuestion,
|
|
1873
|
-
},
|
|
1874
|
-
bossReplies: { [state.stateId]: reply },
|
|
1875
|
-
},
|
|
1876
|
-
}));
|
|
1877
|
-
input = probed.input;
|
|
1878
|
-
promptReads = promptSentinelFields(state, reads, probed.fields, roles);
|
|
1879
|
-
continuation = composeForState(opts.compose, state, input, roles);
|
|
1880
|
-
if (typeof continuation !== 'string') {
|
|
1881
|
-
throw new Error(`${composerName} returned a non-string value`);
|
|
2065
|
+
// Probe absent and explicit fresh modes separately: wrappers may forward
|
|
2066
|
+
// the optional argument incorrectly. Historical composers may ignore it
|
|
2067
|
+
// and keep their complete Q&A prefix even when a player resumes.
|
|
2068
|
+
for (const resuming of modes) {
|
|
2069
|
+
const question = sentinelFor('question');
|
|
2070
|
+
const reply = sentinelFor('bossReply');
|
|
2071
|
+
let continuation;
|
|
2072
|
+
let input;
|
|
2073
|
+
try {
|
|
2074
|
+
const probed = probeContinuationInput(state, inputFn, initial, artifactSchema);
|
|
2075
|
+
input = probed.input;
|
|
2076
|
+
promptReads = promptSentinelFields(state, reads, probed.fields, roles);
|
|
2077
|
+
continuation = composeForState(opts.compose, state, input, roles, resuming);
|
|
2078
|
+
if (typeof continuation !== 'string') {
|
|
2079
|
+
throw new Error(`${composerName} returned a non-string value`);
|
|
2080
|
+
}
|
|
1882
2081
|
}
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
continue;
|
|
1887
|
-
}
|
|
1888
|
-
if (!carriesSentinel(input, question) || !carriesSentinel(input, reply)) {
|
|
1889
|
-
findings.push(`${state.stateId}: invoke.input does not carry pendingBossQuestion/bossReply for a continuation turn`);
|
|
1890
|
-
continue;
|
|
1891
|
-
}
|
|
1892
|
-
if (!continuation.startsWith(`${CONTINUATION_PREAMBLE}\n\n`)) {
|
|
1893
|
-
findings.push(`${state.stateId}: a continuation turn does not open with the exact preamble`);
|
|
1894
|
-
}
|
|
1895
|
-
const bodyStart = bodyIndex(state, continuation, substituted, promptReads);
|
|
1896
|
-
const questionBlock = `${BOSS_QUESTION_LABEL}\n${question}`;
|
|
1897
|
-
const replyBlock = `${BOSS_REPLY_LABEL}\n${reply}`;
|
|
1898
|
-
for (const [label, value] of [
|
|
1899
|
-
[BOSS_QUESTION_LABEL, questionBlock],
|
|
1900
|
-
[BOSS_REPLY_LABEL, replyBlock],
|
|
1901
|
-
]) {
|
|
1902
|
-
// The composer must ADD the labelled block (beyond any body-carried
|
|
1903
|
-
// occurrence), with its sentinel value immediately below the label and
|
|
1904
|
-
// before the body.
|
|
1905
|
-
const at = continuation.indexOf(value);
|
|
1906
|
-
if (occurrences(continuation, value) <= occurrences(state.prompt, value)) {
|
|
1907
|
-
findings.push(`${state.stateId}: a continuation turn lacks the "${label}" block`);
|
|
2082
|
+
catch (error) {
|
|
2083
|
+
pushUnique(findings, `${state.stateId}: ${composerName} threw on a continuation turn: ${messageOf(error)}`);
|
|
2084
|
+
continue;
|
|
1908
2085
|
}
|
|
1909
|
-
|
|
1910
|
-
|
|
2086
|
+
const inputFinding = continuationInputFinding(state.stateId, input, question, reply);
|
|
2087
|
+
if (inputFinding !== undefined) {
|
|
2088
|
+
pushUnique(findings, inputFinding);
|
|
2089
|
+
continue;
|
|
1911
2090
|
}
|
|
2091
|
+
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));
|
|
1912
2092
|
}
|
|
1913
|
-
const exactContinuationPrefix = `${CONTINUATION_PREAMBLE}\n\n${questionBlock}\n\n${replyBlock}\n\n`;
|
|
1914
|
-
if (!continuation.startsWith(exactContinuationPrefix)) {
|
|
1915
|
-
findings.push(`${state.stateId}: a continuation turn does not preserve the exact ordered Boss question/reply blocks`);
|
|
1916
|
-
}
|
|
1917
|
-
findings.push(...bodyFindings(state, continuation, substituted, promptReads, 'continuation'));
|
|
1918
|
-
pushUnique(findings, ...promptControlFindings(state, continuation));
|
|
1919
2093
|
}
|
|
1920
2094
|
return findings;
|
|
1921
2095
|
}
|
|
@@ -1925,7 +2099,7 @@ function promptSentinelFields(state, reads, placeholderFields, declaredRoles) {
|
|
|
1925
2099
|
? fields
|
|
1926
2100
|
: [...fields, ...declaredRoles.map((role) => `promptIdentity:${role}`)];
|
|
1927
2101
|
}
|
|
1928
|
-
function composeForState(compose, state, input, declaredRoles) {
|
|
2102
|
+
function composeForState(compose, state, input, declaredRoles, resuming) {
|
|
1929
2103
|
// Schema-1 composers and the shared default composer use their second
|
|
1930
2104
|
// positional argument as a placeholder-field map. A callable proxy with a
|
|
1931
2105
|
// property-clean view is therefore both an invocation-scoped schema-3 lookup
|
|
@@ -1950,9 +2124,64 @@ function composeForState(compose, state, input, declaredRoles) {
|
|
|
1950
2124
|
getOwnPropertyDescriptor: () => undefined,
|
|
1951
2125
|
});
|
|
1952
2126
|
return state.actor === 'player' && state.role !== undefined
|
|
1953
|
-
?
|
|
2127
|
+
? resuming === undefined
|
|
2128
|
+
? compose(input, promptIdentity)
|
|
2129
|
+
: compose(input, promptIdentity, resuming)
|
|
1954
2130
|
: compose(input);
|
|
1955
2131
|
}
|
|
2132
|
+
function continuationPrefixFindings(state, composed, substituted, reads, artifactSchema, resuming, question, reply) {
|
|
2133
|
+
const fullProfiles = [
|
|
2134
|
+
{ preamble: CONTINUATION_PREAMBLE, questionLabel: BOSS_QUESTION_LABEL },
|
|
2135
|
+
...(artifactSchema === 3
|
|
2136
|
+
? [
|
|
2137
|
+
{
|
|
2138
|
+
preamble: CURRENT_CONTINUATION_PREAMBLE,
|
|
2139
|
+
questionLabel: CURRENT_BOSS_QUESTION_LABEL,
|
|
2140
|
+
},
|
|
2141
|
+
]
|
|
2142
|
+
: []),
|
|
2143
|
+
];
|
|
2144
|
+
const compactAllowed = artifactSchema === 3 && state.actor === 'player' && resuming === true;
|
|
2145
|
+
const replyBlock = `${BOSS_REPLY_LABEL}\n${reply}`;
|
|
2146
|
+
const prefixes = fullProfiles.map(({ preamble, questionLabel }) => `${preamble}\n\n${questionLabel}\n${question}\n\n${replyBlock}\n\n`);
|
|
2147
|
+
const compactPrefix = `${CURRENT_CONTINUATION_PREAMBLE}\n\n${replyBlock}\n\n`;
|
|
2148
|
+
const compact = compactAllowed && composed.startsWith(compactPrefix);
|
|
2149
|
+
if (compactAllowed)
|
|
2150
|
+
prefixes.push(compactPrefix);
|
|
2151
|
+
const findings = [];
|
|
2152
|
+
if (compact &&
|
|
2153
|
+
[BOSS_QUESTION_LABEL, CURRENT_BOSS_QUESTION_LABEL].some((label) => occurrences(composed, `${label}\n${question}`) >
|
|
2154
|
+
occurrences(state.prompt, `${label}\n${question}`))) {
|
|
2155
|
+
findings.push(`${state.stateId}: a compact continuation adds the Boss question outside its domain prompt body`);
|
|
2156
|
+
}
|
|
2157
|
+
if (!fullProfiles.some(({ preamble }) => composed.startsWith(`${preamble}\n\n`))) {
|
|
2158
|
+
findings.push(`${state.stateId}: a continuation turn does not open with the exact preamble`);
|
|
2159
|
+
}
|
|
2160
|
+
// Select diagnostics by the observed full format, without permitting a
|
|
2161
|
+
// hybrid label or using function arity/package versions as capability evidence.
|
|
2162
|
+
const profile = fullProfiles.find(({ preamble }) => composed.startsWith(`${preamble}\n\n`)) ?? fullProfiles[0];
|
|
2163
|
+
const blocks = [
|
|
2164
|
+
...(!compact
|
|
2165
|
+
? [[profile.questionLabel, `${profile.questionLabel}\n${question}`]]
|
|
2166
|
+
: []),
|
|
2167
|
+
[BOSS_REPLY_LABEL, replyBlock],
|
|
2168
|
+
];
|
|
2169
|
+
const bodyStart = bodyIndex(state, composed, substituted, reads);
|
|
2170
|
+
for (const [label, value] of blocks) {
|
|
2171
|
+
// Authored domain text can itself quote the framework contract; a real
|
|
2172
|
+
// continuation must add its own sentinel-bearing blocks before that body.
|
|
2173
|
+
if (occurrences(composed, value) <= occurrences(state.prompt, value)) {
|
|
2174
|
+
findings.push(`${state.stateId}: a continuation turn lacks the "${label}" block`);
|
|
2175
|
+
}
|
|
2176
|
+
else if (bodyStart !== -1 && composed.indexOf(value) > bodyStart) {
|
|
2177
|
+
findings.push(`${state.stateId}: the "${label}" block appears after the domain prompt body`);
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
if (!prefixes.some((prefix) => composed.startsWith(prefix))) {
|
|
2181
|
+
findings.push(`${state.stateId}: a continuation turn does not preserve the exact ordered Boss question/reply blocks`);
|
|
2182
|
+
}
|
|
2183
|
+
return findings;
|
|
2184
|
+
}
|
|
1956
2185
|
function promptControlFindings(state, composed) {
|
|
1957
2186
|
const findings = [];
|
|
1958
2187
|
const introducesPlayerBinding = patternOccurrences(composed, PLAYER_BINDING_MARKER) >
|
|
@@ -2046,7 +2275,12 @@ function matchPromptBody(prompt, composed, reads, expectedSubstitutions) {
|
|
|
2046
2275
|
if (attempt === null)
|
|
2047
2276
|
continue;
|
|
2048
2277
|
if (expectedSubstitutions !== undefined) {
|
|
2049
|
-
return {
|
|
2278
|
+
return {
|
|
2279
|
+
index: start,
|
|
2280
|
+
end: attempt.end,
|
|
2281
|
+
substitutions: [...expectedSubstitutions],
|
|
2282
|
+
values: attempt.values,
|
|
2283
|
+
};
|
|
2050
2284
|
}
|
|
2051
2285
|
const modes = new Map();
|
|
2052
2286
|
for (let gap = 0; gap < tokens.length; gap++) {
|
|
@@ -2059,10 +2293,246 @@ function matchPromptBody(prompt, composed, reads, expectedSubstitutions) {
|
|
|
2059
2293
|
continue;
|
|
2060
2294
|
}
|
|
2061
2295
|
const substitutions = placeholdersIn(prompt).filter((token) => modes.get(token)?.has('sentinel') === true);
|
|
2062
|
-
return {
|
|
2296
|
+
return {
|
|
2297
|
+
index: start,
|
|
2298
|
+
end: attempt.end,
|
|
2299
|
+
substitutions,
|
|
2300
|
+
values: attempt.values,
|
|
2301
|
+
};
|
|
2063
2302
|
}
|
|
2064
2303
|
return null;
|
|
2065
2304
|
}
|
|
2305
|
+
/** Probe observed string relays without inventing context fields or input shapes. */
|
|
2306
|
+
function literalPromptRelayFindings(state, composed, input, substituted, reads, compose, roles, findingKeys, resuming) {
|
|
2307
|
+
if (typeof input !== 'object' || input === null)
|
|
2308
|
+
return [];
|
|
2309
|
+
const match = matchPromptBody(state.prompt, composed, reads, substituted);
|
|
2310
|
+
if (match === null)
|
|
2311
|
+
return []; // Existing body diagnostics own this case.
|
|
2312
|
+
const tokens = [...state.prompt.matchAll(PLACEHOLDER)];
|
|
2313
|
+
const groups = new Map();
|
|
2314
|
+
for (const [field, value] of Object.entries(input)) {
|
|
2315
|
+
if (CONTRACT_INPUT_FIELDS.includes(field) ||
|
|
2316
|
+
typeof value !== 'string' ||
|
|
2317
|
+
roles.some((role) => value === sentinelFor(`promptIdentity:${role}`)) ||
|
|
2318
|
+
!match.values.some((matched, index) => matched === value && substituted.includes(tokens[index][0])) ||
|
|
2319
|
+
match.values.includes(JSON.stringify(value)))
|
|
2320
|
+
continue;
|
|
2321
|
+
// Equal values may be intentional aliases. Changing them together avoids
|
|
2322
|
+
// guessing which of several indistinguishable fields supplied the token.
|
|
2323
|
+
const fields = groups.get(value) ?? [];
|
|
2324
|
+
fields.push(field);
|
|
2325
|
+
groups.set(value, fields);
|
|
2326
|
+
}
|
|
2327
|
+
if (groups.size === 0)
|
|
2328
|
+
return [];
|
|
2329
|
+
const quotedGroups = new Set([...groups.keys()].filter((value) => tokens.every((token, index) => {
|
|
2330
|
+
if (match.values[index] !== value)
|
|
2331
|
+
return true;
|
|
2332
|
+
const start = state.prompt.lastIndexOf('\n', token.index - 1) + 1;
|
|
2333
|
+
const next = state.prompt.indexOf('\n', token.index);
|
|
2334
|
+
const line = state.prompt
|
|
2335
|
+
.slice(start, next < 0 ? undefined : next)
|
|
2336
|
+
.replace(/\r$/, '');
|
|
2337
|
+
return /^> [^<>\r\n]*(<[^\s<>`]{1,60}>)$/.exec(line)?.[1] === token[0];
|
|
2338
|
+
})));
|
|
2339
|
+
const findings = [];
|
|
2340
|
+
for (const multiline of [false, true]) {
|
|
2341
|
+
const values = new Map();
|
|
2342
|
+
for (const [index, value] of [...groups.keys()].entries()) {
|
|
2343
|
+
if (multiline && !quotedGroups.has(value))
|
|
2344
|
+
continue;
|
|
2345
|
+
const literal = `relay-${index}: $& $$ $\` $' ${placeholdersIn(state.prompt).join(' ')}`;
|
|
2346
|
+
// No empty-line policy is inferred here; all segments are nonempty.
|
|
2347
|
+
values.set(value, multiline ? `${literal}\nsecond-${index}\r\nthird-${index}` : literal);
|
|
2348
|
+
}
|
|
2349
|
+
if (values.size === 0)
|
|
2350
|
+
continue;
|
|
2351
|
+
const probed = { ...input };
|
|
2352
|
+
for (const [value, replacement] of values) {
|
|
2353
|
+
for (const field of groups.get(value))
|
|
2354
|
+
probed[field] = replacement;
|
|
2355
|
+
}
|
|
2356
|
+
let index = 0;
|
|
2357
|
+
const body = state.prompt.replace(PLACEHOLDER, () => {
|
|
2358
|
+
const value = match.values[index++];
|
|
2359
|
+
const replacement = values.get(value);
|
|
2360
|
+
return replacement === undefined
|
|
2361
|
+
? value
|
|
2362
|
+
: multiline
|
|
2363
|
+
? replacement.replace(/\n/g, '\n> ')
|
|
2364
|
+
: replacement;
|
|
2365
|
+
});
|
|
2366
|
+
const expected = composed.slice(0, match.index) + body + composed.slice(match.end);
|
|
2367
|
+
const label = multiline
|
|
2368
|
+
? 'multiline quoted-relay'
|
|
2369
|
+
: 'single-pass literal-relay';
|
|
2370
|
+
const key = `${state.stateId}:${label}`;
|
|
2371
|
+
const prefix = `${state.stateId}: prompt composition does not preserve ${label} text`;
|
|
2372
|
+
try {
|
|
2373
|
+
const actual = composeForState(compose, state, probed, roles, resuming);
|
|
2374
|
+
if (actual === expected)
|
|
2375
|
+
continue;
|
|
2376
|
+
pushRelayFinding(findings, findingKeys, key, `${prefix}; probe preserves literal text and LF/CRLF separators/quote markers; ${firstDifferenceDiagnostic(expected, actual)}`);
|
|
2377
|
+
}
|
|
2378
|
+
catch {
|
|
2379
|
+
// One state/representation diagnostic also covers mode-dependent errors.
|
|
2380
|
+
pushRelayFinding(findings, findingKeys, key, prefix);
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
return findings;
|
|
2384
|
+
}
|
|
2385
|
+
function pushRelayFinding(findings, findingKeys, key, finding) {
|
|
2386
|
+
if (findingKeys.has(key))
|
|
2387
|
+
return;
|
|
2388
|
+
findingKeys.add(key);
|
|
2389
|
+
findings.push(finding);
|
|
2390
|
+
}
|
|
2391
|
+
function firstDifferenceDiagnostic(expected, actual) {
|
|
2392
|
+
let offset = 0;
|
|
2393
|
+
const length = Math.max(expected.length, actual.length);
|
|
2394
|
+
while (offset < length && expected[offset] === actual[offset])
|
|
2395
|
+
offset++;
|
|
2396
|
+
return `first difference at UTF-16 offset ${offset}: expected ${boundedJsonSnippet(expected, offset)}, actual ${boundedJsonSnippet(actual, offset)}`;
|
|
2397
|
+
}
|
|
2398
|
+
function boundedJsonSnippet(value, offset) {
|
|
2399
|
+
if (offset >= value.length)
|
|
2400
|
+
return '<end>';
|
|
2401
|
+
return JSON.stringify(value.slice(offset, offset + 24));
|
|
2402
|
+
}
|
|
2403
|
+
/** Verify the FSM-owned composer; child inputs reach the bridge already rendered. */
|
|
2404
|
+
function literalPlaybookTextFinding(prompt, binding, initial) {
|
|
2405
|
+
const input = binding.invoke.input;
|
|
2406
|
+
// Only explicit quoted relay notation establishes required runtime slots.
|
|
2407
|
+
// Domain metavariables and inline-code examples remain literal template text.
|
|
2408
|
+
const requiredRelays = new Set(prompt.split(/\r?\n/).flatMap((line) => {
|
|
2409
|
+
const token = /^> (?:[^<>\r\n]+: )?(<[^\s<>`]{1,60}>)$/.exec(line)?.[1];
|
|
2410
|
+
return token === undefined ? [] : [token];
|
|
2411
|
+
}));
|
|
2412
|
+
const unresolvedRelay = (token) => `leaves quoted child-input relay ${token} unresolved`;
|
|
2413
|
+
if (typeof input !== 'function') {
|
|
2414
|
+
if (binding.state.text !== prompt)
|
|
2415
|
+
return 'does not preserve the complete GEARS child-input template';
|
|
2416
|
+
const token = requiredRelays.values().next().value;
|
|
2417
|
+
return token === undefined ? undefined : unresolvedRelay(token);
|
|
2418
|
+
}
|
|
2419
|
+
const base = initial ?? {};
|
|
2420
|
+
const fields = new Set();
|
|
2421
|
+
let context = { ...base };
|
|
2422
|
+
// Later reads can be behind a string-valued branch. Discover them without
|
|
2423
|
+
// overwriting an initialized object, array, boolean, or numeric context field.
|
|
2424
|
+
for (let pass = 0; pass < 4; pass++) {
|
|
2425
|
+
const before = fields.size;
|
|
2426
|
+
for (const field of probeContextReads(input, context)) {
|
|
2427
|
+
if (base[field] === undefined || typeof base[field] === 'string')
|
|
2428
|
+
fields.add(field);
|
|
2429
|
+
}
|
|
2430
|
+
if (fields.size > 128)
|
|
2431
|
+
return 'has unsupported composition (too many scalar reads)';
|
|
2432
|
+
context = {
|
|
2433
|
+
...base,
|
|
2434
|
+
...Object.fromEntries([...fields].map((field) => [field, sentinelFor(field)])),
|
|
2435
|
+
};
|
|
2436
|
+
if (fields.size === before)
|
|
2437
|
+
break;
|
|
2438
|
+
if (pass === 3)
|
|
2439
|
+
return 'has unsupported composition (context reads did not stabilize)';
|
|
2440
|
+
}
|
|
2441
|
+
const probe = (values) => {
|
|
2442
|
+
const result = invocationInput(binding.invoke, values);
|
|
2443
|
+
return 'value' in result &&
|
|
2444
|
+
result.value.playbookId === binding.state.playbookId &&
|
|
2445
|
+
result.value.stateId === binding.state.stateId &&
|
|
2446
|
+
typeof result.value.text === 'string'
|
|
2447
|
+
? result.value.text
|
|
2448
|
+
: undefined;
|
|
2449
|
+
};
|
|
2450
|
+
const text = probe(context);
|
|
2451
|
+
if (text === undefined)
|
|
2452
|
+
return 'has unsupported composition (input evaluation or literal identity changed)';
|
|
2453
|
+
const matched = matchPromptBody(prompt, text, [...fields]);
|
|
2454
|
+
if (matched === null || matched.index !== 0 || matched.end !== text.length) {
|
|
2455
|
+
return 'does not preserve the complete GEARS child-input template';
|
|
2456
|
+
}
|
|
2457
|
+
const tokens = [...prompt.matchAll(PLACEHOLDER)].map((match) => match[0]);
|
|
2458
|
+
const mapping = new Map();
|
|
2459
|
+
const observed = new Map();
|
|
2460
|
+
for (let index = 0; index < tokens.length; index++) {
|
|
2461
|
+
const token = tokens[index], value = matched.values[index];
|
|
2462
|
+
if (observed.has(token) && observed.get(token) !== value) {
|
|
2463
|
+
return `uses inconsistent context substitutions for ${token}`;
|
|
2464
|
+
}
|
|
2465
|
+
observed.set(token, value);
|
|
2466
|
+
if (value === token) {
|
|
2467
|
+
if (requiredRelays.has(token))
|
|
2468
|
+
return unresolvedRelay(token);
|
|
2469
|
+
continue;
|
|
2470
|
+
}
|
|
2471
|
+
const field = [...fields].find((name) => value === sentinelFor(name));
|
|
2472
|
+
if (field === undefined)
|
|
2473
|
+
return `has unsupported composition for ${token}`;
|
|
2474
|
+
mapping.set(token, { field });
|
|
2475
|
+
}
|
|
2476
|
+
// A source-labelled complete relay can carry arbitrary prose. Inline slots
|
|
2477
|
+
// can instead be constrained identifiers (for example a commit identity),
|
|
2478
|
+
// so do not invent multiline identifier inputs merely because TS erased the
|
|
2479
|
+
// narrower domain contract. Neither decision depends on the field's name.
|
|
2480
|
+
const multilineFields = new Set();
|
|
2481
|
+
for (const line of prompt.split(/\r?\n/)) {
|
|
2482
|
+
const token = /^> [^<>\r\n]*(<[^\s<>`]{1,60}>)$/.exec(line)?.[1];
|
|
2483
|
+
const field = token === undefined ? undefined : mapping.get(token)?.field;
|
|
2484
|
+
if (field !== undefined)
|
|
2485
|
+
multilineFields.add(field);
|
|
2486
|
+
}
|
|
2487
|
+
const payloads = Object.fromEntries([...fields].map((field) => [
|
|
2488
|
+
field,
|
|
2489
|
+
`«${field}:literal» $& $$ $\x60 $' ${[...new Set(tokens)].join(' ')}` +
|
|
2490
|
+
(multilineFields.has(field) ? `\n\n«${field}:second»` : ''),
|
|
2491
|
+
]));
|
|
2492
|
+
const render = (values) => {
|
|
2493
|
+
// Only the source-defined standalone relay form owns empty-line removal.
|
|
2494
|
+
const template = prompt
|
|
2495
|
+
.split(/(?<=\n)/)
|
|
2496
|
+
.filter((line) => {
|
|
2497
|
+
const token = /^> (<[^\s<>`]{1,60}>)(?:\r?\n)?$/.exec(line)?.[1];
|
|
2498
|
+
const mapped = token === undefined ? undefined : mapping.get(token);
|
|
2499
|
+
return mapped === undefined || values[mapped.field] !== '';
|
|
2500
|
+
})
|
|
2501
|
+
.join('');
|
|
2502
|
+
return template.replace(PLACEHOLDER, (token, offset) => {
|
|
2503
|
+
const mapped = mapping.get(token);
|
|
2504
|
+
if (mapped === undefined)
|
|
2505
|
+
return token;
|
|
2506
|
+
const value = values[mapped.field];
|
|
2507
|
+
const lineStart = template.lastIndexOf('\n', offset - 1) + 1;
|
|
2508
|
+
return template.startsWith('> ', lineStart)
|
|
2509
|
+
? value.replace(/\r?\n/g, (separator) => `${separator}> `)
|
|
2510
|
+
: value;
|
|
2511
|
+
});
|
|
2512
|
+
};
|
|
2513
|
+
const composed = probe({ ...base, ...payloads });
|
|
2514
|
+
if (composed === undefined || composed !== render(payloads)) {
|
|
2515
|
+
return 'does not preserve literal substitutions and quoted continuation lines';
|
|
2516
|
+
}
|
|
2517
|
+
const emptyFields = new Set();
|
|
2518
|
+
for (const line of prompt.split(/\r?\n/)) {
|
|
2519
|
+
const token = /^> (<[^\s<>`]{1,60}>)$/.exec(line)?.[1];
|
|
2520
|
+
const field = token === undefined ? undefined : mapping.get(token)?.field;
|
|
2521
|
+
if (field !== undefined)
|
|
2522
|
+
emptyFields.add(field);
|
|
2523
|
+
}
|
|
2524
|
+
if (emptyFields.size > 0) {
|
|
2525
|
+
const values = {
|
|
2526
|
+
...payloads,
|
|
2527
|
+
...Object.fromEntries([...emptyFields].map((field) => [field, ''])),
|
|
2528
|
+
};
|
|
2529
|
+
const emptyText = probe({ ...base, ...values });
|
|
2530
|
+
if (emptyText === undefined || emptyText !== render(values)) {
|
|
2531
|
+
return 'does not preserve source-defined empty standalone relay omission';
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
return undefined;
|
|
2535
|
+
}
|
|
2066
2536
|
/** Findings when a composed prompt does not preserve the domain body (verification-5). */
|
|
2067
2537
|
function bodyFindings(state, composed, substituted, reads, turn) {
|
|
2068
2538
|
if (matchPromptBody(state.prompt, composed, reads, substituted) !== null) {
|
|
@@ -2151,7 +2621,7 @@ import { describe, expect, it } from 'vitest';
|
|
|
2151
2621
|
import { checkGearsFsmConformance, findConcurrentRoleSets, findMachineConfig } from ${sourceString(opts.verifyModule)};
|
|
2152
2622
|
import * as fsm from ${sourceString(opts.fsmModule)};
|
|
2153
2623
|
|
|
2154
|
-
const SCHEMA_FINDINGS = ${JSON.stringify(opts.schemaFindings ?? [], null, 2)};
|
|
2624
|
+
const SCHEMA_FINDINGS: readonly string[] = ${JSON.stringify(opts.schemaFindings ?? [], null, 2)};
|
|
2155
2625
|
|
|
2156
2626
|
describe(${sourceString(`${opts.basename}: GEARS↔FSM conformance`)}, () => {
|
|
2157
2627
|
it('uses consistent artifact-schema evidence', () => {
|
|
@@ -2262,7 +2732,7 @@ import { describe, expect, it } from 'vitest';
|
|
|
2262
2732
|
import { findMachineConfig, pinIntrospection } from ${sourceString(opts.verifyModule)};
|
|
2263
2733
|
import * as fsm from ${sourceString(opts.fsmModule)};
|
|
2264
2734
|
|
|
2265
|
-
const PINNED = ${JSON.stringify(opts.pins, null, 2)};
|
|
2735
|
+
const PINNED = ${JSON.stringify(opts.pins, null, 2)} as const;
|
|
2266
2736
|
|
|
2267
2737
|
describe(${sourceString(`${opts.basename}: FSM introspection`)}, () => {
|
|
2268
2738
|
it('matches the machine topology pinned at build time', () => {
|
|
@@ -2294,7 +2764,7 @@ export function generatePromptContractTest(opts) {
|
|
|
2294
2764
|
const compose = `compose${label === 'Captain' ? 'Captain' : 'Player'}`;
|
|
2295
2765
|
return [
|
|
2296
2766
|
`
|
|
2297
|
-
const ${constant} = ${JSON.stringify(substituted, null, 2)};
|
|
2767
|
+
const ${constant} = ${JSON.stringify(substituted, null, 2)} as const;
|
|
2298
2768
|
|
|
2299
2769
|
const ${compose} = (
|
|
2300
2770
|
playbook as unknown as {
|
|
@@ -2344,8 +2814,8 @@ import {
|
|
|
2344
2814
|
} from ${sourceString(opts.verifyModule)};
|
|
2345
2815
|
import * as fsm from ${sourceString(opts.fsmModule)};
|
|
2346
2816
|
${composerImports}
|
|
2347
|
-
const CONTRACT = ${JSON.stringify(opts.rows, null, 2)};
|
|
2348
|
-
const SCHEMA_FINDINGS = ${JSON.stringify(opts.schemaFindings ?? [], null, 2)};
|
|
2817
|
+
const CONTRACT = ${JSON.stringify(opts.rows, null, 2)} as const;
|
|
2818
|
+
const SCHEMA_FINDINGS: readonly string[] = ${JSON.stringify(opts.schemaFindings ?? [], null, 2)};
|
|
2349
2819
|
|
|
2350
2820
|
describe(${sourceString(`${opts.basename}: prompt contract`)}, () => {
|
|
2351
2821
|
it('uses consistent artifact-schema evidence', () => {
|
|
@@ -2482,7 +2952,8 @@ export function resolveArtifactSchemaForVerification(opts) {
|
|
|
2482
2952
|
if (linkedSignal?.historicalFallback) {
|
|
2483
2953
|
return { artifactSchema: 1, findings: [] };
|
|
2484
2954
|
}
|
|
2485
|
-
const hasAmbiguousCaptainContinuation = opts.
|
|
2955
|
+
const hasAmbiguousCaptainContinuation = opts.requireContinuationSchema !== false &&
|
|
2956
|
+
opts.config !== undefined &&
|
|
2486
2957
|
enumerateCaptainStates(opts.config).some((state) => state.actor === 'captain' &&
|
|
2487
2958
|
Object.hasOwn(state.result, NEEDS_BOSS_REPLY));
|
|
2488
2959
|
return hasAmbiguousCaptainContinuation
|