@sublang/slc 0.5.0 → 0.6.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 +4 -4
- package/dist/app.d.ts +20 -20
- package/dist/app.js +29 -29
- package/dist/app.js.map +1 -1
- package/dist/artifacts.d.ts +6 -6
- package/dist/artifacts.d.ts.map +1 -1
- package/dist/artifacts.js +9 -9
- package/dist/artifacts.js.map +1 -1
- package/dist/cli.js +3 -3
- package/dist/cligent-agent.d.ts +1 -1
- package/dist/cligent-agent.js +5 -5
- package/dist/compiled-executor.d.ts +14 -6
- package/dist/compiled-executor.d.ts.map +1 -1
- package/dist/compiled-executor.js +133 -31
- package/dist/compiled-executor.js.map +1 -1
- package/dist/config-file.d.ts +6 -6
- package/dist/config-file.js +5 -5
- package/dist/config.d.ts +17 -15
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +26 -16
- package/dist/config.js.map +1 -1
- package/dist/emitted-imports.d.ts +14 -0
- package/dist/emitted-imports.d.ts.map +1 -1
- package/dist/emitted-imports.js +63 -7
- package/dist/emitted-imports.js.map +1 -1
- package/dist/entry-module.d.ts +5 -3
- package/dist/entry-module.d.ts.map +1 -1
- package/dist/entry-module.js +41 -13
- package/dist/entry-module.js.map +1 -1
- package/dist/execution.d.ts +10 -10
- package/dist/execution.d.ts.map +1 -1
- package/dist/execution.js +13 -13
- package/dist/execution.js.map +1 -1
- package/dist/hash.js +2 -2
- package/dist/host-capabilities.d.ts +115 -0
- package/dist/host-capabilities.d.ts.map +1 -0
- package/dist/host-capabilities.js +507 -0
- package/dist/host-capabilities.js.map +1 -0
- package/dist/interpreter.d.ts +2 -2
- package/dist/interpreter.d.ts.map +1 -1
- package/dist/interpreter.js +7 -7
- package/dist/invocation.d.ts +6 -6
- package/dist/invocation.d.ts.map +1 -1
- package/dist/invocation.js +1 -1
- package/dist/link.d.ts +4 -4
- package/dist/link.d.ts.map +1 -1
- package/dist/link.js +11 -11
- package/dist/link.js.map +1 -1
- package/dist/phase-runner.d.ts +4 -4
- package/dist/phase-runner.js +9 -9
- package/dist/phase.d.ts +4 -4
- package/dist/phase.d.ts.map +1 -1
- package/dist/phase.js +7 -7
- package/dist/phase.js.map +1 -1
- package/dist/pin-closure.d.ts +35 -7
- package/dist/pin-closure.d.ts.map +1 -1
- package/dist/pin-closure.js +135 -21
- package/dist/pin-closure.js.map +1 -1
- package/dist/pin-currency.d.ts +9 -4
- package/dist/pin-currency.d.ts.map +1 -1
- package/dist/pin-currency.js +57 -20
- package/dist/pin-currency.js.map +1 -1
- package/dist/pin-generate.d.ts +7 -7
- package/dist/pin-generate.d.ts.map +1 -1
- package/dist/pin-generate.js +14 -8
- package/dist/pin-generate.js.map +1 -1
- package/dist/pin-inputs.d.ts +41 -0
- package/dist/pin-inputs.d.ts.map +1 -0
- package/dist/pin-inputs.js +179 -0
- package/dist/pin-inputs.js.map +1 -0
- package/dist/pin-paths.d.ts +1 -1
- package/dist/pin-paths.js +4 -4
- package/dist/pin-paths.js.map +1 -1
- package/dist/pins.d.ts +4 -4
- package/dist/pins.d.ts.map +1 -1
- package/dist/pins.js +5 -5
- package/dist/pins.js.map +1 -1
- package/dist/pipeline.d.ts +5 -5
- package/dist/pipeline.js +10 -10
- package/dist/playbook-contract.d.ts +53 -5
- package/dist/playbook-contract.d.ts.map +1 -1
- package/dist/playbook-contract.js +70 -21
- package/dist/playbook-contract.js.map +1 -1
- package/dist/playbook-ports.d.ts +9 -9
- package/dist/playbook-ports.d.ts.map +1 -1
- package/dist/playbook-ports.js +3 -3
- package/dist/playbook-ports.js.map +1 -1
- package/dist/progress.d.ts +8 -9
- package/dist/progress.d.ts.map +1 -1
- package/dist/progress.js +3 -3
- package/dist/progress.js.map +1 -1
- package/dist/resolver.d.ts +9 -9
- package/dist/resolver.d.ts.map +1 -1
- package/dist/resolver.js +12 -12
- package/dist/resolver.js.map +1 -1
- package/dist/runner.d.ts +1 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +195 -80
- package/dist/runner.js.map +1 -1
- package/dist/verify-coverage.d.ts +4 -3
- package/dist/verify-coverage.d.ts.map +1 -1
- package/dist/verify-coverage.js +509 -71
- package/dist/verify-coverage.js.map +1 -1
- package/dist/verify.d.ts +111 -15
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js +656 -59
- package/dist/verify.js.map +1 -1
- package/package.json +3 -3
package/dist/verify.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
|
|
3
3
|
/**
|
|
4
4
|
* Compilation-correctness verification for a compiled `playbook` artifact
|
|
5
|
-
* (
|
|
5
|
+
* (DR-009).
|
|
6
6
|
*
|
|
7
7
|
* A compiled artifact is a judgment-produced program, so `slc` re-checks it
|
|
8
8
|
* against its source. The GEARS↔FSM conformance check verifies that every GEARS
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* inputs; {@link generateGearsFsmConformanceTest} emits a per-artifact test that
|
|
17
17
|
* runs it beside the artifacts. The checker reads the `text2gears` item format
|
|
18
18
|
* and the `gears2fsm` `invoke.input` contract, not any one artifact, so it holds
|
|
19
|
-
* for every compiled `playbook`. See specs/
|
|
19
|
+
* for every compiled `playbook`. See specs/packages/verification.md.
|
|
20
20
|
*/
|
|
21
21
|
import { randomUUID } from 'node:crypto';
|
|
22
22
|
import { existsSync } from 'node:fs';
|
|
@@ -32,6 +32,49 @@ import { hashFile } from './hash.js';
|
|
|
32
32
|
*/
|
|
33
33
|
export const NEEDS_BOSS_REPLY = 'needsBossReply';
|
|
34
34
|
export const BOSS_QUESTION_MARKER = 'Output shall include `question:';
|
|
35
|
+
/** Artifact schema selected only by a complete reviewed Playbook provenance. */
|
|
36
|
+
export function artifactSchemaForPlaybookProvenance(provenance) {
|
|
37
|
+
switch (provenance) {
|
|
38
|
+
case '@sublang/playbook@0.10.0':
|
|
39
|
+
case '@sublang/playbook@1.0.0':
|
|
40
|
+
case '@sublang/playbook@2.0.0':
|
|
41
|
+
case '@sublang/playbook@3.1.0':
|
|
42
|
+
case '@sublang/playbook@4.0.0':
|
|
43
|
+
return 1;
|
|
44
|
+
case '@sublang/playbook@10.0.0':
|
|
45
|
+
return 3;
|
|
46
|
+
default:
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Returns the Playbook package provenance that owns an invocation's concrete
|
|
52
|
+
* link target. The first package manifest above the target owns the file; a
|
|
53
|
+
* parent workspace manifest must not lend its identity to a nested local file.
|
|
54
|
+
*/
|
|
55
|
+
export async function playbookProvenanceForLinkTarget(linkTarget) {
|
|
56
|
+
let cursor = dirname(resolve(linkTarget));
|
|
57
|
+
for (;;) {
|
|
58
|
+
const manifestPath = join(cursor, 'package.json');
|
|
59
|
+
if (existsSync(manifestPath)) {
|
|
60
|
+
try {
|
|
61
|
+
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
|
|
62
|
+
return manifest.name === '@sublang/playbook' &&
|
|
63
|
+
typeof manifest.version === 'string' &&
|
|
64
|
+
manifest.version.length > 0
|
|
65
|
+
? `${manifest.name}@${manifest.version}`
|
|
66
|
+
: undefined;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
const parent = dirname(cursor);
|
|
73
|
+
if (parent === cursor)
|
|
74
|
+
return undefined;
|
|
75
|
+
cursor = parent;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
35
78
|
const ITEM_HEADING = /^###\s+([A-Za-z][\w-]*)\s*$/;
|
|
36
79
|
// The `text2gears` item form names a delegated player as "Captain shall prompt
|
|
37
80
|
// <Player>" (or a "relay ... to <Player>" variant); English players are
|
|
@@ -41,6 +84,7 @@ const ITEM_HEADING = /^###\s+([A-Za-z][\w-]*)\s*$/;
|
|
|
41
84
|
const ITEM_PLAYER = /Captain shall (?:prompt|relay\b[^.]*?\bto)\s+(?:`([^`]+)`|"([^"]+)"|“([^”]+)”|([A-Z][\w]*)|([^\p{ASCII}][^\s::,,。;;]*))/u;
|
|
42
85
|
const ITEM_PLAYBOOK = /Captain shall call playbook\s+(?:`([^`]+)`|"([^"]+)"|“([^”]+)”|([A-Za-z0-9][\w.-]*))\s*:/;
|
|
43
86
|
const ITEM_DYNAMIC_PLAYBOOK = /Captain shall call playbook selected by\s+`([^`]+)`\s*:/;
|
|
87
|
+
const PARALLEL_GROUP = /^Parallel group:\s*(\S(?:.*\S)?)\s*$/;
|
|
44
88
|
const DYNAMIC_TEXT = /^<([A-Za-z_$][A-Za-z0-9_$]*)>$/;
|
|
45
89
|
// An optimizer-introduced script item runs a shell command without any agent
|
|
46
90
|
// (text2gears.md "Script behaviors"; DR-013). The clause is fixed machine
|
|
@@ -112,6 +156,9 @@ export function parseGearsItems(gears) {
|
|
|
112
156
|
...(dynamicText === null ? {} : { textContext: dynamicText[1] }),
|
|
113
157
|
}
|
|
114
158
|
: {}),
|
|
159
|
+
...(current.parallelGroup !== ''
|
|
160
|
+
? { parallelGroup: current.parallelGroup }
|
|
161
|
+
: {}),
|
|
115
162
|
...(current.resultDeclared
|
|
116
163
|
? { result: Object.fromEntries(current.results) }
|
|
117
164
|
: {}),
|
|
@@ -133,6 +180,7 @@ export function parseGearsItems(gears) {
|
|
|
133
180
|
script: false,
|
|
134
181
|
playbookId: '',
|
|
135
182
|
playbookIdContext: '',
|
|
183
|
+
parallelGroup: '',
|
|
136
184
|
prompt: [],
|
|
137
185
|
resultsEligible: false,
|
|
138
186
|
resultDeclared: false,
|
|
@@ -195,6 +243,10 @@ export function parseGearsItems(gears) {
|
|
|
195
243
|
}
|
|
196
244
|
if (line.trim() !== '')
|
|
197
245
|
current.resultsEligible = false;
|
|
246
|
+
const parallelGroup = PARALLEL_GROUP.exec(line.trim());
|
|
247
|
+
if (parallelGroup !== null && current.parallelGroup === '') {
|
|
248
|
+
current.parallelGroup = parallelGroup[1];
|
|
249
|
+
}
|
|
198
250
|
const player = ITEM_PLAYER.exec(line);
|
|
199
251
|
if (player !== null && current.player === '') {
|
|
200
252
|
current.player =
|
|
@@ -221,6 +273,152 @@ export function parseGearsItems(gears) {
|
|
|
221
273
|
flush();
|
|
222
274
|
return items;
|
|
223
275
|
}
|
|
276
|
+
const ROLE_DECLARATION = /^(?:#{1,6}\s+(Roles|Players)|(Roles|Players):)\s*$/;
|
|
277
|
+
/** Canonical lowercase local-role id used by schema-3 artifacts. */
|
|
278
|
+
export function canonicalRoleId(name) {
|
|
279
|
+
return name.toLowerCase();
|
|
280
|
+
}
|
|
281
|
+
function declarationName(value) {
|
|
282
|
+
const match = /^[`"“]?([^`"”]+?)[`"”]?\s*$/.exec(value.trim());
|
|
283
|
+
return match?.[1].trim() || undefined;
|
|
284
|
+
}
|
|
285
|
+
/** Parses Roles/Players plus source-derived concurrent role sets without host bindings. */
|
|
286
|
+
export function inspectGearsRoleContract(gears) {
|
|
287
|
+
const findings = [];
|
|
288
|
+
const declarations = [];
|
|
289
|
+
let active;
|
|
290
|
+
for (const line of gears.split('\n')) {
|
|
291
|
+
const heading = ROLE_DECLARATION.exec(line.trim());
|
|
292
|
+
if (heading !== null) {
|
|
293
|
+
active = {
|
|
294
|
+
kind: (heading[1] ?? heading[2]),
|
|
295
|
+
names: [],
|
|
296
|
+
};
|
|
297
|
+
declarations.push(active);
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (active === undefined)
|
|
301
|
+
continue;
|
|
302
|
+
if (line.trim() === '')
|
|
303
|
+
continue;
|
|
304
|
+
const bullet = /^-\s+(.*)$/.exec(line.trim());
|
|
305
|
+
if (bullet === null) {
|
|
306
|
+
active = undefined;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
const declaration = bullet[1].trim();
|
|
310
|
+
if (active.kind === 'Roles' && /[=|]/.test(declaration)) {
|
|
311
|
+
findings.push(`Roles declaration ${JSON.stringify(declaration)} uses removed alias syntax`);
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
// Historical schema-1 Players may declare a composite launcher choice
|
|
315
|
+
// (`Committer = Coder | Reviewer`). It is not one concrete player binding
|
|
316
|
+
// and therefore does not enter the source-order player list, including
|
|
317
|
+
// when every name is backtick- or quote-delimited.
|
|
318
|
+
if (active.kind === 'Players' && declaration.includes('='))
|
|
319
|
+
continue;
|
|
320
|
+
const name = declarationName(declaration);
|
|
321
|
+
if (name === undefined) {
|
|
322
|
+
findings.push(`malformed ${active.kind} declaration ${JSON.stringify(declaration)}`);
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
active.names.push(name);
|
|
326
|
+
}
|
|
327
|
+
const kinds = new Set(declarations.map(({ kind }) => kind));
|
|
328
|
+
if (declarations.length > 1) {
|
|
329
|
+
findings.push('GEARS declares more than one Roles/Players section');
|
|
330
|
+
}
|
|
331
|
+
if (kinds.size > 1) {
|
|
332
|
+
findings.push('GEARS mixes Roles and Players declarations');
|
|
333
|
+
}
|
|
334
|
+
const selected = declarations[0];
|
|
335
|
+
const generation = selected?.kind === 'Roles'
|
|
336
|
+
? 'schema-3'
|
|
337
|
+
: selected?.kind === 'Players'
|
|
338
|
+
? 'schema-1'
|
|
339
|
+
: 'unspecified';
|
|
340
|
+
const names = selected?.names ?? [];
|
|
341
|
+
const roleIds = names.map(canonicalRoleId);
|
|
342
|
+
if (generation === 'schema-3') {
|
|
343
|
+
const byCanonical = new Map();
|
|
344
|
+
for (let index = 0; index < names.length; index += 1) {
|
|
345
|
+
const name = names[index];
|
|
346
|
+
const roleId = roleIds[index];
|
|
347
|
+
const existing = byCanonical.get(roleId);
|
|
348
|
+
if (existing !== undefined) {
|
|
349
|
+
findings.push(existing === name
|
|
350
|
+
? `Roles declaration repeats ${JSON.stringify(name)}`
|
|
351
|
+
: `Roles declarations ${JSON.stringify(existing)} and ${JSON.stringify(name)} collide as canonical role ${JSON.stringify(roleId)}`);
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
byCanonical.set(roleId, name);
|
|
355
|
+
}
|
|
356
|
+
// A canonical local role id is the declared name lowercased. Playbook 10
|
|
357
|
+
// explicitly admits non-English role names ("quote non-English names
|
|
358
|
+
// (e.g., `作者`)"), and a script without case - Chinese among them -
|
|
359
|
+
// lowercases to itself, so an ASCII-only class would reject exactly the
|
|
360
|
+
// names the definition sanctions. Require instead that the id be genuinely
|
|
361
|
+
// canonical: already lowercase, carrying no whitespace or separator that
|
|
362
|
+
// would make it ambiguous as an identifier.
|
|
363
|
+
if (roleId.length === 0 ||
|
|
364
|
+
roleId !== roleId.toLowerCase() ||
|
|
365
|
+
/[\s.,;:/\\'"`()[\]{}<>|]/u.test(roleId)) {
|
|
366
|
+
findings.push(`Roles declaration ${JSON.stringify(name)} derives noncanonical local role ${JSON.stringify(roleId)}`);
|
|
367
|
+
}
|
|
368
|
+
else if (roleId === 'captain') {
|
|
369
|
+
findings.push('Roles declaration uses reserved local role "captain"');
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
const groups = new Map();
|
|
374
|
+
if (generation === 'schema-3') {
|
|
375
|
+
const declared = new Set(roleIds);
|
|
376
|
+
for (const item of parseGearsItems(gears)) {
|
|
377
|
+
if (item.actor === 'player') {
|
|
378
|
+
const roleId = canonicalRoleId(item.player);
|
|
379
|
+
if (!declared.has(roleId)) {
|
|
380
|
+
findings.push(`GEARS item ${item.id} delegates to undeclared role ${JSON.stringify(item.player)}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (item.parallelGroup === undefined)
|
|
384
|
+
continue;
|
|
385
|
+
if (item.actor !== 'player') {
|
|
386
|
+
findings.push(`parallel group ${JSON.stringify(item.parallelGroup)} contains non-role item ${item.id}`);
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
const roleId = canonicalRoleId(item.player);
|
|
390
|
+
const members = groups.get(item.parallelGroup) ?? [];
|
|
391
|
+
if (members.includes(roleId)) {
|
|
392
|
+
findings.push(`parallel group ${JSON.stringify(item.parallelGroup)} repeats canonical role ${JSON.stringify(roleId)}`);
|
|
393
|
+
}
|
|
394
|
+
members.push(roleId);
|
|
395
|
+
groups.set(item.parallelGroup, members);
|
|
396
|
+
}
|
|
397
|
+
const groupByMembers = new Map();
|
|
398
|
+
for (const [group, members] of groups) {
|
|
399
|
+
if (members.length < 2) {
|
|
400
|
+
findings.push(`parallel group ${JSON.stringify(group)} contains fewer than two roles`);
|
|
401
|
+
}
|
|
402
|
+
// A concurrent role set is unordered for duplicate detection even
|
|
403
|
+
// though its source member order remains significant in the FSM export.
|
|
404
|
+
const signature = JSON.stringify([...members].sort());
|
|
405
|
+
const existing = groupByMembers.get(signature);
|
|
406
|
+
if (existing !== undefined) {
|
|
407
|
+
findings.push(`parallel groups ${JSON.stringify(existing)} and ${JSON.stringify(group)} duplicate concurrent role set ${signature}`);
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
groupByMembers.set(signature, group);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return {
|
|
415
|
+
generation,
|
|
416
|
+
names,
|
|
417
|
+
roleIds,
|
|
418
|
+
concurrentRoleSets: [...groups.values()],
|
|
419
|
+
findings,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
224
422
|
/** Walks every state node depth-first in declaration order. */
|
|
225
423
|
function walkStateNodes(config) {
|
|
226
424
|
const out = [];
|
|
@@ -277,6 +475,14 @@ function metadataStateId(state) {
|
|
|
277
475
|
const stateId = playbook.stateId;
|
|
278
476
|
return isNonEmptyString(stateId) ? stateId : undefined;
|
|
279
477
|
}
|
|
478
|
+
function metadataRole(state) {
|
|
479
|
+
if (typeof state.meta !== 'object' || state.meta === null)
|
|
480
|
+
return undefined;
|
|
481
|
+
const playbook = state.meta.playbook;
|
|
482
|
+
if (typeof playbook !== 'object' || playbook === null)
|
|
483
|
+
return undefined;
|
|
484
|
+
return playbook.role;
|
|
485
|
+
}
|
|
280
486
|
function stateIdConsistencyFindings(node, inputStateId) {
|
|
281
487
|
if (!isNonEmptyString(inputStateId))
|
|
282
488
|
return [];
|
|
@@ -359,11 +565,11 @@ function enumerateCaptainBindings(config) {
|
|
|
359
565
|
(invoke.src !== undefined || !isNonEmptyString(fields.sourceItem))) {
|
|
360
566
|
continue;
|
|
361
567
|
}
|
|
362
|
-
// Published artifacts
|
|
363
|
-
//
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
568
|
+
// Published schema-1 artifacts carried a concrete player field. Schema 3
|
|
569
|
+
// carries only its canonical local role and repeats it in public metadata.
|
|
570
|
+
const actor = source === 'player' ||
|
|
571
|
+
Object.hasOwn(fields, 'player') ||
|
|
572
|
+
Object.hasOwn(fields, 'role')
|
|
367
573
|
? 'player'
|
|
368
574
|
: 'captain';
|
|
369
575
|
const pinActor = source === 'player' || (source === 'captain' && actor === 'captain');
|
|
@@ -371,8 +577,37 @@ function enumerateCaptainBindings(config) {
|
|
|
371
577
|
if (!isNonEmptyString(fields.sourceItem)) {
|
|
372
578
|
bindingFindings.push('invoke.input.sourceItem is not a non-empty string');
|
|
373
579
|
}
|
|
374
|
-
if (actor === 'player'
|
|
375
|
-
|
|
580
|
+
if (actor === 'player') {
|
|
581
|
+
const hasPlayer = Object.hasOwn(fields, 'player');
|
|
582
|
+
const hasRole = Object.hasOwn(fields, 'role');
|
|
583
|
+
if (hasPlayer && hasRole) {
|
|
584
|
+
bindingFindings.push('invoke.input carries both historical player and schema-3 role');
|
|
585
|
+
}
|
|
586
|
+
if (hasRole && typeof fields.role !== 'string') {
|
|
587
|
+
bindingFindings.push('invoke.input.role is not a string');
|
|
588
|
+
}
|
|
589
|
+
if (hasRole && source !== 'player') {
|
|
590
|
+
bindingFindings.push('schema-3 delegated work does not invoke the player actor');
|
|
591
|
+
}
|
|
592
|
+
if (!hasRole && typeof fields.player !== 'string') {
|
|
593
|
+
bindingFindings.push('invoke.input.player is not a string');
|
|
594
|
+
}
|
|
595
|
+
const publicRole = metadataRole(node.state);
|
|
596
|
+
if (hasRole) {
|
|
597
|
+
if (typeof publicRole !== 'string') {
|
|
598
|
+
bindingFindings.push('state.meta.playbook.role is not a string for schema-3 delegated work');
|
|
599
|
+
}
|
|
600
|
+
else if (publicRole !== fields.role) {
|
|
601
|
+
bindingFindings.push(`state.meta.playbook.role ${JSON.stringify(publicRole)} does not match invoke.input.role ${JSON.stringify(fields.role)}`);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
else if (publicRole !== undefined) {
|
|
605
|
+
bindingFindings.push('historical delegated-player state unexpectedly declares state.meta.playbook.role');
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
else if (Object.hasOwn(fields, 'role') ||
|
|
609
|
+
metadataRole(node.state) !== undefined) {
|
|
610
|
+
bindingFindings.push('direct-Captain state unexpectedly declares a role binding');
|
|
376
611
|
}
|
|
377
612
|
if (typeof fields.prompt !== 'string') {
|
|
378
613
|
bindingFindings.push('invoke.input.prompt is not a string');
|
|
@@ -395,6 +630,7 @@ function enumerateCaptainBindings(config) {
|
|
|
395
630
|
: '',
|
|
396
631
|
actor,
|
|
397
632
|
player: typeof fields.player === 'string' ? fields.player : '',
|
|
633
|
+
...(typeof fields.role === 'string' ? { role: fields.role } : {}),
|
|
398
634
|
prompt: typeof fields.prompt === 'string' ? fields.prompt : '',
|
|
399
635
|
result: resultMap(fields.result),
|
|
400
636
|
...nestedStatePath(node),
|
|
@@ -655,24 +891,115 @@ function statePlaybookSignature(state) {
|
|
|
655
891
|
state.textContext ?? null,
|
|
656
892
|
]);
|
|
657
893
|
}
|
|
894
|
+
/** Exact action guards of Playbook 10's controller decision result. */
|
|
895
|
+
export const CONTROLLER_ACTION_GUARDS = [
|
|
896
|
+
'respond',
|
|
897
|
+
'resume',
|
|
898
|
+
'start',
|
|
899
|
+
'switch',
|
|
900
|
+
'dismiss',
|
|
901
|
+
'deliver',
|
|
902
|
+
'runtime',
|
|
903
|
+
];
|
|
904
|
+
/** Whether a result map has the exact Playbook 10 controller domain union. */
|
|
905
|
+
export function isControllerDecisionResult(result) {
|
|
906
|
+
if (!isStringMap(result))
|
|
907
|
+
return false;
|
|
908
|
+
const keys = Object.keys(result).filter((key) => key !== NEEDS_BOSS_REPLY);
|
|
909
|
+
return (keys.length === CONTROLLER_ACTION_GUARDS.length &&
|
|
910
|
+
CONTROLLER_ACTION_GUARDS.every((guard) => Object.hasOwn(result, guard)));
|
|
911
|
+
}
|
|
912
|
+
/** A single missing or extra key against Playbook 10's controller domain. */
|
|
913
|
+
export function controllerDecisionNearMiss(result) {
|
|
914
|
+
if (!isStringMap(result))
|
|
915
|
+
return undefined;
|
|
916
|
+
const actual = Object.keys(result).filter((key) => key !== NEEDS_BOSS_REPLY);
|
|
917
|
+
const expected = new Set(CONTROLLER_ACTION_GUARDS);
|
|
918
|
+
const present = new Set(actual);
|
|
919
|
+
const missing = CONTROLLER_ACTION_GUARDS.filter((key) => !present.has(key));
|
|
920
|
+
const extra = actual.filter((key) => !expected.has(key));
|
|
921
|
+
return missing.length + extra.length === 1 ? { missing, extra } : undefined;
|
|
922
|
+
}
|
|
923
|
+
/** Whether a machine contains Playbook 10's grounded controller decision state. */
|
|
924
|
+
export function isControllerMachine(config) {
|
|
925
|
+
return enumerateCaptainBindings(config).some(({ state, pinActor }) => {
|
|
926
|
+
if (!pinActor || state.actor !== 'captain')
|
|
927
|
+
return false;
|
|
928
|
+
if (state.bindingFindings?.includes('invoke.input.result is not a string-valued object')) {
|
|
929
|
+
return false;
|
|
930
|
+
}
|
|
931
|
+
return isControllerDecisionResult(state.result);
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
/** Whether an explicit direct-Captain result is one key from the controller domain. */
|
|
935
|
+
export function hasControllerDecisionNearMiss(config) {
|
|
936
|
+
return enumerateCaptainBindings(config).some(({ state, pinActor }) => pinActor &&
|
|
937
|
+
state.actor === 'captain' &&
|
|
938
|
+
state.result[NEEDS_BOSS_REPLY] === undefined &&
|
|
939
|
+
controllerDecisionNearMiss(state.result) !== undefined);
|
|
940
|
+
}
|
|
941
|
+
function concurrentRoleSets(value) {
|
|
942
|
+
if (!Array.isArray(value))
|
|
943
|
+
return undefined;
|
|
944
|
+
const sets = [];
|
|
945
|
+
for (const candidate of value) {
|
|
946
|
+
if (!Array.isArray(candidate) ||
|
|
947
|
+
candidate.some((role) => typeof role !== 'string')) {
|
|
948
|
+
return undefined;
|
|
949
|
+
}
|
|
950
|
+
sets.push([...candidate]);
|
|
951
|
+
}
|
|
952
|
+
return sets;
|
|
953
|
+
}
|
|
658
954
|
/**
|
|
659
955
|
* Checks GEARS↔FSM conformance and returns human-readable findings (empty when
|
|
660
956
|
* conformant): every GEARS item maps to one state with the same player and the
|
|
661
957
|
* prompt verbatim, every captain state references a known item, and every
|
|
662
958
|
* captain state's `result` map declares the Boss-reply suspension key with its
|
|
663
|
-
* adjudicator contract (
|
|
959
|
+
* adjudicator contract (verification-1, verification-3; DR-009).
|
|
664
960
|
*/
|
|
665
|
-
export function checkGearsFsmConformance(gears, config) {
|
|
961
|
+
export function checkGearsFsmConformance(gears, config, options = {}) {
|
|
666
962
|
const items = parseGearsItems(gears);
|
|
963
|
+
const roleContract = inspectGearsRoleContract(gears);
|
|
964
|
+
const controller = isControllerMachine(config);
|
|
667
965
|
const captainBindings = enumerateCaptainBindings(config);
|
|
668
966
|
const states = captainBindings.map(({ state }) => state);
|
|
967
|
+
const controllerNearMisses = captainBindings.flatMap(({ state, pinActor }) => {
|
|
968
|
+
if (!pinActor ||
|
|
969
|
+
state.actor !== 'captain' ||
|
|
970
|
+
state.result[NEEDS_BOSS_REPLY] !== undefined) {
|
|
971
|
+
return [];
|
|
972
|
+
}
|
|
973
|
+
const nearMiss = controllerDecisionNearMiss(state.result);
|
|
974
|
+
return nearMiss === undefined ? [] : [{ state, nearMiss }];
|
|
975
|
+
});
|
|
976
|
+
const controllerNearMissStates = new Set(controllerNearMisses.map(({ state }) => state));
|
|
669
977
|
const explicitActorStates = new Set(captainBindings
|
|
670
978
|
.filter(({ pinActor }) => pinActor)
|
|
671
979
|
.map(({ state }) => state));
|
|
672
980
|
const playbookStates = enumeratePlaybookStates(config);
|
|
673
981
|
const scriptStates = enumerateScriptStates(config);
|
|
674
982
|
const findings = [];
|
|
983
|
+
for (const { state, nearMiss } of controllerNearMisses) {
|
|
984
|
+
const detail = nearMiss.missing.length > 0
|
|
985
|
+
? `missing ${JSON.stringify(nearMiss.missing[0])}`
|
|
986
|
+
: `extra ${JSON.stringify(nearMiss.extra[0])}`;
|
|
987
|
+
findings.push(`FSM state ${state.stateId}: controller decision contract near-miss (${detail}); the controller domain requires exactly ${CONTROLLER_ACTION_GUARDS.join(', ')}`);
|
|
988
|
+
}
|
|
675
989
|
findings.push(...structuredStateIdentityFindings(walkStateNodes(config)));
|
|
990
|
+
findings.push(...roleContract.findings);
|
|
991
|
+
const requiresConcurrentRoleSets = roleContract.generation === 'schema-3' ||
|
|
992
|
+
controller ||
|
|
993
|
+
options.artifactSchema === 3;
|
|
994
|
+
if (requiresConcurrentRoleSets) {
|
|
995
|
+
const actual = concurrentRoleSets(options.concurrentRoleSets);
|
|
996
|
+
if (actual === undefined) {
|
|
997
|
+
findings.push('schema-3 FSM exports no valid concurrentRoleSets array');
|
|
998
|
+
}
|
|
999
|
+
else if (JSON.stringify(actual) !== JSON.stringify(roleContract.concurrentRoleSets)) {
|
|
1000
|
+
findings.push(`schema-3 FSM concurrentRoleSets ${JSON.stringify(actual)} do not match GEARS groups ${JSON.stringify(roleContract.concurrentRoleSets)}`);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
676
1003
|
for (const item of items) {
|
|
677
1004
|
findings.push(...(item.resultFindings ?? []).map((finding) => `GEARS item ${item.id}: ${finding}`));
|
|
678
1005
|
}
|
|
@@ -840,8 +1167,24 @@ export function checkGearsFsmConformance(gears, config) {
|
|
|
840
1167
|
state.actor !== item.actor) {
|
|
841
1168
|
findings.push(`${item.id}: FSM actor "${state.actor}" is not GEARS actor "${item.actor}"`);
|
|
842
1169
|
}
|
|
843
|
-
if (item.actor === 'player'
|
|
844
|
-
|
|
1170
|
+
if (item.actor === 'player') {
|
|
1171
|
+
if (roleContract.generation === 'schema-3') {
|
|
1172
|
+
const expectedRole = canonicalRoleId(item.player);
|
|
1173
|
+
if (state.role !== expectedRole) {
|
|
1174
|
+
findings.push(`${item.id}: FSM role ${JSON.stringify(state.role ?? '')} is not GEARS canonical role ${JSON.stringify(expectedRole)}`);
|
|
1175
|
+
}
|
|
1176
|
+
if (state.player !== '') {
|
|
1177
|
+
findings.push(`${item.id}: schema-3 delegated role carries removed invoke.input.player ${JSON.stringify(state.player)}`);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
else {
|
|
1181
|
+
if (state.player !== item.player) {
|
|
1182
|
+
findings.push(`${item.id}: FSM player "${state.player}" is not GEARS player "${item.player}"`);
|
|
1183
|
+
}
|
|
1184
|
+
if (state.role !== undefined) {
|
|
1185
|
+
findings.push(`${item.id}: historical delegated player unexpectedly carries invoke.input.role ${JSON.stringify(state.role)}`);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
845
1188
|
}
|
|
846
1189
|
if (state.prompt !== item.prompt) {
|
|
847
1190
|
findings.push(`${item.id}: FSM prompt is not the GEARS prompt verbatim`);
|
|
@@ -871,11 +1214,19 @@ export function checkGearsFsmConformance(gears, config) {
|
|
|
871
1214
|
if (state.sourceItem !== '' && !itemIds.has(state.sourceItem)) {
|
|
872
1215
|
findings.push(`FSM state ${state.stateId} references unknown GEARS item ${state.sourceItem}`);
|
|
873
1216
|
}
|
|
874
|
-
// Every captain-invoking state supports Boss-reply suspension: its result
|
|
875
|
-
// map carries `needsBossReply` with the adjudicator-facing contract text
|
|
876
|
-
// (gears2fsm.md; VERIFY-3).
|
|
877
1217
|
const bossReply = state.result[NEEDS_BOSS_REPLY];
|
|
878
|
-
if (
|
|
1218
|
+
if (controller) {
|
|
1219
|
+
if (bossReply !== undefined) {
|
|
1220
|
+
findings.push(`FSM controller state ${state.stateId} unexpectedly declares ${NEEDS_BOSS_REPLY}`);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
else if (controllerNearMissStates.has(state)) {
|
|
1224
|
+
// The precise controller-domain diagnostic above owns a malformed
|
|
1225
|
+
// near-controller state; do not suggest adding the ordinary wait key to
|
|
1226
|
+
// that state, while retaining ordinary findings for every other state.
|
|
1227
|
+
continue;
|
|
1228
|
+
}
|
|
1229
|
+
else if (bossReply === undefined) {
|
|
879
1230
|
findings.push(`FSM state ${state.stateId} declares no ${NEEDS_BOSS_REPLY} result`);
|
|
880
1231
|
}
|
|
881
1232
|
else if (!bossReply.includes(BOSS_QUESTION_MARKER)) {
|
|
@@ -898,7 +1249,7 @@ export function checkGearsFsmConformance(gears, config) {
|
|
|
898
1249
|
return findings;
|
|
899
1250
|
}
|
|
900
1251
|
/*
|
|
901
|
-
* Machine introspection (
|
|
1252
|
+
* Machine introspection (verification-4).
|
|
902
1253
|
*
|
|
903
1254
|
* `pinIntrospection` reduces a machine config to its structural facts — the
|
|
904
1255
|
* captain-state bindings, every transition arm, the root and quiescent event
|
|
@@ -963,7 +1314,7 @@ function invokeSource(src) {
|
|
|
963
1314
|
}
|
|
964
1315
|
/**
|
|
965
1316
|
* Reduces a machine config to the structural facts the emitted introspection
|
|
966
|
-
* test pins (
|
|
1317
|
+
* test pins (verification-4): captain bindings with result keys and every transition
|
|
967
1318
|
* arm, the quiescent states' event surfaces, the root event surface, and the
|
|
968
1319
|
* `BOSS_INTERRUPT` jumpable set.
|
|
969
1320
|
*/
|
|
@@ -990,6 +1341,7 @@ export function pinIntrospection(config) {
|
|
|
990
1341
|
: {}),
|
|
991
1342
|
sourceItem: binding.state.sourceItem,
|
|
992
1343
|
player: binding.state.player,
|
|
1344
|
+
...(binding.state.role !== undefined ? { role: binding.state.role } : {}),
|
|
993
1345
|
resultKeys: Object.keys(binding.state.result).sort(),
|
|
994
1346
|
onDone: normalizeArms(binding.invoke.onDone),
|
|
995
1347
|
onError: normalizeArms(binding.invoke.onError),
|
|
@@ -1061,7 +1413,7 @@ export function pinIntrospection(config) {
|
|
|
1061
1413
|
};
|
|
1062
1414
|
}
|
|
1063
1415
|
/*
|
|
1064
|
-
* Prompt-contract capture and composition checks (
|
|
1416
|
+
* Prompt-contract capture and composition checks (verification-5).
|
|
1065
1417
|
*
|
|
1066
1418
|
* The contract is derived from the artifacts, never hand-authored: context
|
|
1067
1419
|
* reads are traced through each state's `invoke.input` thunk with a recording
|
|
@@ -1079,6 +1431,7 @@ export const BOSS_REPLY_LABEL = 'Boss reply:';
|
|
|
1079
1431
|
// labelled form as well as natural-language variants; occurrence deltas below
|
|
1080
1432
|
// keep self-hosting prompt bodies free to quote either marker verbatim.
|
|
1081
1433
|
const PLAYER_BINDING_MARKER = /\bplayer\s+binding\b|(?:^|\n)[ \t]*player[ \t]*:[ \t]*(?=\S)/gi;
|
|
1434
|
+
const ROLE_BINDING_MARKER = /\brole\s+binding\b|(?:^|\n)[ \t]*role[ \t]*:[ \t]*(?=\S)/gi;
|
|
1082
1435
|
const PLAYER_RESUME_MARKER = /\b(?:resume|resuming)\b[^\n]{0,120}\bplayer(?:'s)?\b|\bplayer(?:'s)?\b[^\n]{0,120}\b(?:resume|resuming)\b/gi;
|
|
1083
1436
|
const PLACEHOLDER = /<[^\s<>`]{1,60}>/g;
|
|
1084
1437
|
/** Lists the distinct `<...>` placeholder tokens in a prompt body, in order. */
|
|
@@ -1139,7 +1492,7 @@ function carriesSentinel(value, sentinel) {
|
|
|
1139
1492
|
}
|
|
1140
1493
|
/**
|
|
1141
1494
|
* Derives every captain state's prompt contract from the machine config
|
|
1142
|
-
* (
|
|
1495
|
+
* (verification-5): traced context reads, sentinel-traced input wiring, and the
|
|
1143
1496
|
* prompt body's placeholder tokens.
|
|
1144
1497
|
*/
|
|
1145
1498
|
export function capturePromptContract(config) {
|
|
@@ -1167,6 +1520,7 @@ export function capturePromptContract(config) {
|
|
|
1167
1520
|
state: state.stateId,
|
|
1168
1521
|
sourceItem: state.sourceItem,
|
|
1169
1522
|
player: state.player,
|
|
1523
|
+
...(state.role !== undefined ? { role: state.role } : {}),
|
|
1170
1524
|
reads,
|
|
1171
1525
|
wires,
|
|
1172
1526
|
placeholders: placeholdersIn(state.prompt),
|
|
@@ -1178,7 +1532,7 @@ export function capturePromptContract(config) {
|
|
|
1178
1532
|
* Derives, per captain state, which of its prompt's placeholder tokens the
|
|
1179
1533
|
* linked composer substitutes when the wired context is present — pinned into
|
|
1180
1534
|
* the emitted test so a token that later leaks unsubstituted fails it
|
|
1181
|
-
* (
|
|
1535
|
+
* (verification-5).
|
|
1182
1536
|
*/
|
|
1183
1537
|
export function deriveSubstitutions(config, compose, actor) {
|
|
1184
1538
|
const out = {};
|
|
@@ -1190,7 +1544,7 @@ export function deriveSubstitutions(config, compose, actor) {
|
|
|
1190
1544
|
continue;
|
|
1191
1545
|
try {
|
|
1192
1546
|
const reads = probeContextReads(inputFn);
|
|
1193
|
-
const composed = compose
|
|
1547
|
+
const composed = composeForState(compose, state, inputFn({ context: ordinaryContext(reads) }));
|
|
1194
1548
|
if (typeof composed !== 'string') {
|
|
1195
1549
|
out[state.stateId] = [];
|
|
1196
1550
|
continue;
|
|
@@ -1201,8 +1555,9 @@ export function deriveSubstitutions(config, compose, actor) {
|
|
|
1201
1555
|
// hide valid evidence, while merely deleting a token still cannot
|
|
1202
1556
|
// masquerade as substitution.
|
|
1203
1557
|
const evidenced = new Set();
|
|
1558
|
+
const promptReads = promptSentinelFields(state, reads);
|
|
1204
1559
|
for (const line of state.prompt.split('\n')) {
|
|
1205
|
-
for (const token of matchPromptBody(line, composed,
|
|
1560
|
+
for (const token of matchPromptBody(line, composed, promptReads)
|
|
1206
1561
|
?.substitutions ?? []) {
|
|
1207
1562
|
evidenced.add(token);
|
|
1208
1563
|
}
|
|
@@ -1217,7 +1572,7 @@ export function deriveSubstitutions(config, compose, actor) {
|
|
|
1217
1572
|
}
|
|
1218
1573
|
/**
|
|
1219
1574
|
* Checks the linked composer against the link contract for every captain state
|
|
1220
|
-
* (
|
|
1575
|
+
* (verification-5), returning findings (empty when conformant): the prompt body is
|
|
1221
1576
|
* preserved modulo substituted placeholders, the adjudicator-facing Boss-reply
|
|
1222
1577
|
* contract never leaks into a player prompt, no continuation appears on an
|
|
1223
1578
|
* ordinary turn, and a Boss-reply continuation turn opens with the exact
|
|
@@ -1225,19 +1580,30 @@ export function deriveSubstitutions(config, compose, actor) {
|
|
|
1225
1580
|
*/
|
|
1226
1581
|
export function checkPromptComposition(opts) {
|
|
1227
1582
|
const findings = [];
|
|
1583
|
+
const controller = isControllerMachine(opts.config);
|
|
1584
|
+
const schemaResolution = resolveArtifactSchemaForVerification({
|
|
1585
|
+
config: opts.config,
|
|
1586
|
+
...(opts.artifactSchema === undefined
|
|
1587
|
+
? {}
|
|
1588
|
+
: { artifactSchema: opts.artifactSchema }),
|
|
1589
|
+
});
|
|
1590
|
+
findings.push(...schemaResolution.findings);
|
|
1591
|
+
const inferredArtifactSchema = schemaResolution.artifactSchema;
|
|
1228
1592
|
const substitutions = deriveSubstitutions(opts.config, opts.compose, opts.actor);
|
|
1229
1593
|
const composerName = opts.actor === 'captain' ? 'composeCaptainPrompt' : 'composePlayerPrompt';
|
|
1230
|
-
|
|
1594
|
+
const bindings = enumerateCaptainBindings(opts.config);
|
|
1595
|
+
for (const binding of bindings) {
|
|
1231
1596
|
const { state, inputFn } = binding;
|
|
1232
1597
|
if (opts.actor !== undefined && state.actor !== opts.actor)
|
|
1233
1598
|
continue;
|
|
1234
1599
|
if (typeof inputFn !== 'function')
|
|
1235
1600
|
continue;
|
|
1236
1601
|
const reads = probeContextReads(inputFn);
|
|
1602
|
+
const promptReads = promptSentinelFields(state, reads);
|
|
1237
1603
|
const substituted = substitutions[state.stateId] ?? [];
|
|
1238
1604
|
let ordinary;
|
|
1239
1605
|
try {
|
|
1240
|
-
ordinary = opts.compose
|
|
1606
|
+
ordinary = composeForState(opts.compose, state, inputFn({ context: ordinaryContext(reads) }));
|
|
1241
1607
|
if (typeof ordinary !== 'string') {
|
|
1242
1608
|
throw new Error(`${composerName} returned a non-string value`);
|
|
1243
1609
|
}
|
|
@@ -1246,8 +1612,8 @@ export function checkPromptComposition(opts) {
|
|
|
1246
1612
|
findings.push(`${state.stateId}: ${composerName} threw on an ordinary turn: ${messageOf(error)}`);
|
|
1247
1613
|
continue;
|
|
1248
1614
|
}
|
|
1249
|
-
findings.push(...bodyFindings(state, ordinary, substituted,
|
|
1250
|
-
pushUnique(findings, ...
|
|
1615
|
+
findings.push(...bodyFindings(state, ordinary, substituted, promptReads, 'ordinary'));
|
|
1616
|
+
pushUnique(findings, ...promptControlFindings(state, ordinary));
|
|
1251
1617
|
// A self-hosted playbook's domain body may legitimately quote the
|
|
1252
1618
|
// adjudicator contract or the continuation texts (it instructs a compiler
|
|
1253
1619
|
// about them); only occurrences the composer ADDS beyond the body's own
|
|
@@ -1259,15 +1625,34 @@ export function checkPromptComposition(opts) {
|
|
|
1259
1625
|
if ([CONTINUATION_PREAMBLE, BOSS_QUESTION_LABEL, BOSS_REPLY_LABEL].some((needle) => occurrences(ordinary, needle) > occurrences(state.prompt, needle))) {
|
|
1260
1626
|
findings.push(`${state.stateId}: continuation blocks appear on an ordinary turn`);
|
|
1261
1627
|
}
|
|
1628
|
+
// Controllers own no Boss-reply wait. A missing ordinary result is already
|
|
1629
|
+
// diagnosed by conformance, so do not fabricate a continuation contract.
|
|
1630
|
+
if (controller || !Object.hasOwn(state.result, NEEDS_BOSS_REPLY))
|
|
1631
|
+
continue;
|
|
1632
|
+
const artifactSchema = schemaResolution.findings.length > 0
|
|
1633
|
+
? undefined
|
|
1634
|
+
: (inferredArtifactSchema ??
|
|
1635
|
+
(state.role !== undefined ? 3 : state.player !== '' ? 1 : undefined));
|
|
1636
|
+
if (artifactSchema === undefined) {
|
|
1637
|
+
findings.push(`${state.stateId}: prompt composition requires artifactSchema 1 or 3 to probe this direct-Captain continuation`);
|
|
1638
|
+
continue;
|
|
1639
|
+
}
|
|
1262
1640
|
// A Boss-reply continuation turn: the thunk carries the pending question
|
|
1263
1641
|
// and reply, and the composer opens with the exact preamble and labelled
|
|
1264
1642
|
// Q&A blocks before the domain body (gears2fsm.md, link.md).
|
|
1265
1643
|
const question = sentinelFor('question');
|
|
1266
1644
|
const reply = sentinelFor('bossReply');
|
|
1267
1645
|
const pendingBossQuestion = {
|
|
1646
|
+
...(artifactSchema === 3
|
|
1647
|
+
? state.actor === 'captain'
|
|
1648
|
+
? { asker: { kind: 'captain' } }
|
|
1649
|
+
: { asker: { kind: 'role', roleId: state.role ?? '' } }
|
|
1650
|
+
: {
|
|
1651
|
+
player: state.actor === 'captain' ? 'Captain' : state.player,
|
|
1652
|
+
}),
|
|
1653
|
+
questionId: state.stateId,
|
|
1268
1654
|
resumeStateId: state.stateId,
|
|
1269
1655
|
sourceItem: state.sourceItem,
|
|
1270
|
-
player: state.player,
|
|
1271
1656
|
question,
|
|
1272
1657
|
};
|
|
1273
1658
|
let continuation;
|
|
@@ -1284,7 +1669,7 @@ export function checkPromptComposition(opts) {
|
|
|
1284
1669
|
bossReplies: { [state.stateId]: reply },
|
|
1285
1670
|
},
|
|
1286
1671
|
});
|
|
1287
|
-
continuation = opts.compose
|
|
1672
|
+
continuation = composeForState(opts.compose, state, input);
|
|
1288
1673
|
if (typeof continuation !== 'string') {
|
|
1289
1674
|
throw new Error(`${composerName} returned a non-string value`);
|
|
1290
1675
|
}
|
|
@@ -1300,7 +1685,7 @@ export function checkPromptComposition(opts) {
|
|
|
1300
1685
|
if (!continuation.startsWith(`${CONTINUATION_PREAMBLE}\n\n`)) {
|
|
1301
1686
|
findings.push(`${state.stateId}: a continuation turn does not open with the exact preamble`);
|
|
1302
1687
|
}
|
|
1303
|
-
const bodyStart = bodyIndex(state, continuation, substituted,
|
|
1688
|
+
const bodyStart = bodyIndex(state, continuation, substituted, promptReads);
|
|
1304
1689
|
const questionBlock = `${BOSS_QUESTION_LABEL}\n${question}`;
|
|
1305
1690
|
const replyBlock = `${BOSS_REPLY_LABEL}\n${reply}`;
|
|
1306
1691
|
for (const [label, value] of [
|
|
@@ -1322,21 +1707,59 @@ export function checkPromptComposition(opts) {
|
|
|
1322
1707
|
if (!continuation.startsWith(exactContinuationPrefix)) {
|
|
1323
1708
|
findings.push(`${state.stateId}: a continuation turn does not preserve the exact ordered Boss question/reply blocks`);
|
|
1324
1709
|
}
|
|
1325
|
-
findings.push(...bodyFindings(state, continuation, substituted,
|
|
1326
|
-
pushUnique(findings, ...
|
|
1710
|
+
findings.push(...bodyFindings(state, continuation, substituted, promptReads, 'continuation'));
|
|
1711
|
+
pushUnique(findings, ...promptControlFindings(state, continuation));
|
|
1327
1712
|
}
|
|
1328
1713
|
return findings;
|
|
1329
1714
|
}
|
|
1330
|
-
function
|
|
1331
|
-
|
|
1332
|
-
|
|
1715
|
+
function promptSentinelFields(state, reads) {
|
|
1716
|
+
return state.role === undefined
|
|
1717
|
+
? [...reads]
|
|
1718
|
+
: [...reads, `promptIdentity:${state.role}`];
|
|
1719
|
+
}
|
|
1720
|
+
function composeForState(compose, state, input) {
|
|
1721
|
+
// Schema-1 composers and the shared default composer use their second
|
|
1722
|
+
// positional argument as a placeholder-field map. A callable proxy with a
|
|
1723
|
+
// property-clean view is therefore both an invocation-scoped schema-3 lookup
|
|
1724
|
+
// and an empty map to historical/default composition, without Function.name,
|
|
1725
|
+
// Function.length, or Function.prototype token collisions.
|
|
1726
|
+
const lookup = (roleId) => {
|
|
1727
|
+
if (state.role === undefined) {
|
|
1728
|
+
throw new Error(`prompt identity lookup used role ${JSON.stringify(roleId)} for a direct-Captain or historical state`);
|
|
1729
|
+
}
|
|
1730
|
+
if (roleId !== state.role) {
|
|
1731
|
+
throw new Error(`prompt identity lookup used role ${JSON.stringify(roleId)} instead of canonical local role ${JSON.stringify(state.role)}`);
|
|
1732
|
+
}
|
|
1733
|
+
return sentinelFor(`promptIdentity:${roleId}`);
|
|
1734
|
+
};
|
|
1735
|
+
const promptIdentity = new Proxy(lookup, {
|
|
1736
|
+
get: () => undefined,
|
|
1737
|
+
has: () => false,
|
|
1738
|
+
ownKeys: () => [],
|
|
1739
|
+
getOwnPropertyDescriptor: () => undefined,
|
|
1740
|
+
});
|
|
1741
|
+
return state.actor === 'player' && state.role !== undefined
|
|
1742
|
+
? compose(input, promptIdentity)
|
|
1743
|
+
: compose(input);
|
|
1744
|
+
}
|
|
1745
|
+
function promptControlFindings(state, composed) {
|
|
1333
1746
|
const findings = [];
|
|
1334
|
-
|
|
1335
|
-
patternOccurrences(state.prompt, PLAYER_BINDING_MARKER)
|
|
1747
|
+
const introducesPlayerBinding = patternOccurrences(composed, PLAYER_BINDING_MARKER) >
|
|
1748
|
+
patternOccurrences(state.prompt, PLAYER_BINDING_MARKER);
|
|
1749
|
+
if (state.actor === 'captain' && introducesPlayerBinding) {
|
|
1336
1750
|
findings.push(`${state.stateId}: composeCaptainPrompt introduces a player binding into a direct-Captain prompt`);
|
|
1337
1751
|
}
|
|
1338
|
-
if (
|
|
1339
|
-
|
|
1752
|
+
else if (state.role !== undefined && introducesPlayerBinding) {
|
|
1753
|
+
findings.push(`${state.stateId}: composePlayerPrompt exposes a concrete player binding in a schema-3 delegated-role prompt`);
|
|
1754
|
+
}
|
|
1755
|
+
if (state.actor === 'captain' &&
|
|
1756
|
+
patternOccurrences(composed, ROLE_BINDING_MARKER) >
|
|
1757
|
+
patternOccurrences(state.prompt, ROLE_BINDING_MARKER)) {
|
|
1758
|
+
findings.push(`${state.stateId}: composeCaptainPrompt introduces a role binding into a direct-Captain prompt`);
|
|
1759
|
+
}
|
|
1760
|
+
if (state.actor === 'captain' &&
|
|
1761
|
+
patternOccurrences(composed, PLAYER_RESUME_MARKER) >
|
|
1762
|
+
patternOccurrences(state.prompt, PLAYER_RESUME_MARKER)) {
|
|
1340
1763
|
findings.push(`${state.stateId}: composeCaptainPrompt introduces a player resume instruction into a direct-Captain prompt`);
|
|
1341
1764
|
}
|
|
1342
1765
|
return findings;
|
|
@@ -1429,7 +1852,7 @@ function matchPromptBody(prompt, composed, reads, expectedSubstitutions) {
|
|
|
1429
1852
|
}
|
|
1430
1853
|
return null;
|
|
1431
1854
|
}
|
|
1432
|
-
/** Findings when a composed prompt does not preserve the domain body (
|
|
1855
|
+
/** Findings when a composed prompt does not preserve the domain body (verification-5). */
|
|
1433
1856
|
function bodyFindings(state, composed, substituted, reads, turn) {
|
|
1434
1857
|
if (matchPromptBody(state.prompt, composed, reads, substituted) !== null) {
|
|
1435
1858
|
return [];
|
|
@@ -1454,7 +1877,7 @@ function bodyFindings(state, composed, substituted, reads, turn) {
|
|
|
1454
1877
|
function bodyIndex(state, composed, substituted, reads) {
|
|
1455
1878
|
return (matchPromptBody(state.prompt, composed, reads, substituted)?.index ?? -1);
|
|
1456
1879
|
}
|
|
1457
|
-
// Local copy: this module is copied verbatim beside the artifact (
|
|
1880
|
+
// Local copy: this module is copied verbatim beside the artifact (verification-12),
|
|
1458
1881
|
// so it may not import a sibling module.
|
|
1459
1882
|
function messageOf(error) {
|
|
1460
1883
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -1492,6 +1915,12 @@ export function findMachineConfig(fsmModule) {
|
|
|
1492
1915
|
}
|
|
1493
1916
|
throw new Error('fsm module exports no XState machine with a `.config.states`');
|
|
1494
1917
|
}
|
|
1918
|
+
/** Reads the schema-3 cohort declaration from an imported FSM module. */
|
|
1919
|
+
export function findConcurrentRoleSets(fsmModule) {
|
|
1920
|
+
if (typeof fsmModule !== 'object' || fsmModule === null)
|
|
1921
|
+
return undefined;
|
|
1922
|
+
return fsmModule.concurrentRoleSets;
|
|
1923
|
+
}
|
|
1495
1924
|
/**
|
|
1496
1925
|
* Builds a per-artifact vitest module that fails when the compiled FSM drifts
|
|
1497
1926
|
* from its GEARS source: it reads the artifact's `gears` file and the machine its
|
|
@@ -1502,22 +1931,35 @@ export function generateGearsFsmConformanceTest(opts) {
|
|
|
1502
1931
|
return `// SPDX-License-Identifier: Apache-2.0
|
|
1503
1932
|
// SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
|
|
1504
1933
|
|
|
1505
|
-
// Generated by slc (
|
|
1934
|
+
// Generated by slc (DR-009): GEARS↔FSM conformance.
|
|
1506
1935
|
import { readFileSync } from 'node:fs';
|
|
1507
1936
|
import { fileURLToPath } from 'node:url';
|
|
1508
1937
|
|
|
1509
1938
|
import { describe, expect, it } from 'vitest';
|
|
1510
1939
|
|
|
1511
|
-
import { checkGearsFsmConformance, findMachineConfig } from ${sourceString(opts.verifyModule)};
|
|
1940
|
+
import { checkGearsFsmConformance, findConcurrentRoleSets, findMachineConfig } from ${sourceString(opts.verifyModule)};
|
|
1512
1941
|
import * as fsm from ${sourceString(opts.fsmModule)};
|
|
1513
1942
|
|
|
1943
|
+
const SCHEMA_FINDINGS = ${JSON.stringify(opts.schemaFindings ?? [], null, 2)};
|
|
1944
|
+
|
|
1514
1945
|
describe(${sourceString(`${opts.basename}: GEARS↔FSM conformance`)}, () => {
|
|
1946
|
+
it('uses consistent artifact-schema evidence', () => {
|
|
1947
|
+
expect(SCHEMA_FINDINGS).toEqual([]);
|
|
1948
|
+
});
|
|
1949
|
+
|
|
1515
1950
|
it('maps every GEARS item to a state with its player and verbatim prompt', () => {
|
|
1516
1951
|
const gears = readFileSync(
|
|
1517
1952
|
fileURLToPath(new URL(${sourceString(opts.gearsFile)}, import.meta.url)),
|
|
1518
1953
|
'utf8',
|
|
1519
1954
|
);
|
|
1520
|
-
expect(
|
|
1955
|
+
expect(
|
|
1956
|
+
checkGearsFsmConformance(gears, findMachineConfig(fsm), {
|
|
1957
|
+
concurrentRoleSets: findConcurrentRoleSets(fsm),
|
|
1958
|
+
${opts.artifactSchema === undefined
|
|
1959
|
+
? ''
|
|
1960
|
+
: `artifactSchema: ${opts.artifactSchema},`}
|
|
1961
|
+
}),
|
|
1962
|
+
).toEqual([]);
|
|
1521
1963
|
});
|
|
1522
1964
|
});
|
|
1523
1965
|
`;
|
|
@@ -1526,7 +1968,7 @@ describe(${sourceString(`${opts.basename}: GEARS↔FSM conformance`)}, () => {
|
|
|
1526
1968
|
* Emits the GEARS↔FSM conformance test as `slc` output beside a compiled
|
|
1527
1969
|
* `playbook` artifact: writes `<basename>.gears-fsm.test.ts` into the artifact
|
|
1528
1970
|
* directory (`<basename>.playbook/`), wiring the artifact's `gears` file and its
|
|
1529
|
-
* `fsm` module's machine to the checker, and returns the written path (
|
|
1971
|
+
* `fsm` module's machine to the checker, and returns the written path (verification-2;
|
|
1530
1972
|
* [DR-009](../decisions/009-slc-playbook-pipeline-compilation.md)).
|
|
1531
1973
|
*/
|
|
1532
1974
|
export async function emitGearsFsmConformanceTest(opts) {
|
|
@@ -1537,6 +1979,10 @@ export async function emitGearsFsmConformanceTest(opts) {
|
|
|
1537
1979
|
fsmModule: `./${opts.basename}.fsm.js`,
|
|
1538
1980
|
gearsFile: `./${opts.basename}.gears.md`,
|
|
1539
1981
|
verifyModule: opts.verifyModule ?? VERIFY_MODULE,
|
|
1982
|
+
...(opts.artifactSchema === undefined
|
|
1983
|
+
? {}
|
|
1984
|
+
: { artifactSchema: opts.artifactSchema }),
|
|
1985
|
+
schemaFindings: opts.schemaFindings,
|
|
1540
1986
|
});
|
|
1541
1987
|
await mkdir(opts.artifactDir, { recursive: true });
|
|
1542
1988
|
const path = join(opts.artifactDir, `${opts.basename}.gears-fsm.test.ts`);
|
|
@@ -1558,14 +2004,15 @@ export async function loadFsmModule(fsmPath) {
|
|
|
1558
2004
|
return import(url.href);
|
|
1559
2005
|
}
|
|
1560
2006
|
/**
|
|
1561
|
-
* Imports
|
|
1562
|
-
*
|
|
1563
|
-
* `./<basename>.fsm.js` edge,
|
|
1564
|
-
* `./<basename>.fsm.ts` exists. Stage a
|
|
1565
|
-
* module specifier points at the
|
|
1566
|
-
*
|
|
2007
|
+
* Imports generated linked TypeScript for emission-time, standalone, or
|
|
2008
|
+
* equivalence review before its sibling FSM has been built to JavaScript.
|
|
2009
|
+
* NodeNext source correctly names the runtime-safe `./<basename>.fsm.js` edge,
|
|
2010
|
+
* but review may run while only `./<basename>.fsm.ts` exists. Stage a
|
|
2011
|
+
* same-directory copy whose one generated module specifier points at the
|
|
2012
|
+
* hashed TypeScript artifact, import that copy, and remove it without changing
|
|
2013
|
+
* the linked source or its production import.
|
|
1567
2014
|
*/
|
|
1568
|
-
async function loadLinkedModuleForVerification(opts) {
|
|
2015
|
+
export async function loadLinkedModuleForVerification(opts) {
|
|
1569
2016
|
const linkedSource = await readFile(opts.linkedPath, 'utf8');
|
|
1570
2017
|
const fsmStem = basename(opts.fsmPath, '.ts');
|
|
1571
2018
|
const runtimeSpecifier = `./${fsmStem}.js`;
|
|
@@ -1590,7 +2037,7 @@ async function loadLinkedModuleForVerification(opts) {
|
|
|
1590
2037
|
}
|
|
1591
2038
|
/**
|
|
1592
2039
|
* Builds a per-artifact vitest module that fails when the machine's structure
|
|
1593
|
-
* drifts from the topology pinned at build time (
|
|
2040
|
+
* drifts from the topology pinned at build time (verification-4).
|
|
1594
2041
|
*/
|
|
1595
2042
|
export function generateFsmIntrospectionTest(opts) {
|
|
1596
2043
|
return `// SPDX-License-Identifier: Apache-2.0
|
|
@@ -1615,7 +2062,7 @@ describe(${sourceString(`${opts.basename}: FSM introspection`)}, () => {
|
|
|
1615
2062
|
}
|
|
1616
2063
|
/**
|
|
1617
2064
|
* Builds a per-artifact vitest module pinning the prompt contract derived from
|
|
1618
|
-
* the artifacts at build time (
|
|
2065
|
+
* the artifacts at build time (verification-5): the per-state context reads, input
|
|
1619
2066
|
* wiring, and placeholders always; and, when the linked module exposes its
|
|
1620
2067
|
* matching Captain/player composers, the composition checks and pinned
|
|
1621
2068
|
* substitution maps.
|
|
@@ -1650,6 +2097,9 @@ const ${compose} = (
|
|
|
1650
2097
|
config: findMachineConfig(fsm),
|
|
1651
2098
|
compose: ${compose},
|
|
1652
2099
|
actor: '${actor}',
|
|
2100
|
+
${opts.artifactSchema === undefined
|
|
2101
|
+
? ''
|
|
2102
|
+
: `artifactSchema: ${opts.artifactSchema},`}
|
|
1653
2103
|
}),
|
|
1654
2104
|
).toEqual([]);
|
|
1655
2105
|
});
|
|
@@ -1684,17 +2134,141 @@ import {
|
|
|
1684
2134
|
import * as fsm from ${sourceString(opts.fsmModule)};
|
|
1685
2135
|
${composerImports}
|
|
1686
2136
|
const CONTRACT = ${JSON.stringify(opts.rows, null, 2)};
|
|
2137
|
+
const SCHEMA_FINDINGS = ${JSON.stringify(opts.schemaFindings ?? [], null, 2)};
|
|
1687
2138
|
|
|
1688
2139
|
describe(${sourceString(`${opts.basename}: prompt contract`)}, () => {
|
|
2140
|
+
it('uses consistent artifact-schema evidence', () => {
|
|
2141
|
+
expect(SCHEMA_FINDINGS).toEqual([]);
|
|
2142
|
+
});
|
|
2143
|
+
|
|
1689
2144
|
it('matches the prompt contract pinned at build time', () => {
|
|
1690
2145
|
expect(capturePromptContract(findMachineConfig(fsm))).toEqual(CONTRACT);
|
|
1691
2146
|
});
|
|
1692
2147
|
${composerBlock}});
|
|
1693
2148
|
`;
|
|
1694
2149
|
}
|
|
2150
|
+
function promptArtifactSchemaSignalsFromConfig(config) {
|
|
2151
|
+
const states = enumerateCaptainStates(config);
|
|
2152
|
+
return {
|
|
2153
|
+
schema1: states.some(({ player }) => player !== ''),
|
|
2154
|
+
schema3: isControllerMachine(config) ||
|
|
2155
|
+
states.some(({ role }) => role !== undefined),
|
|
2156
|
+
};
|
|
2157
|
+
}
|
|
2158
|
+
function linkedArtifactSchemaSignal(linked) {
|
|
2159
|
+
const factory = linked.default;
|
|
2160
|
+
if (typeof factory !== 'function') {
|
|
2161
|
+
return { historicalFallback: false, invalidCompatibility: false };
|
|
2162
|
+
}
|
|
2163
|
+
if (!Object.hasOwn(factory, 'compat')) {
|
|
2164
|
+
return { historicalFallback: true, invalidCompatibility: false };
|
|
2165
|
+
}
|
|
2166
|
+
const descriptor = Object.getOwnPropertyDescriptor(factory, 'compat');
|
|
2167
|
+
if (descriptor === undefined ||
|
|
2168
|
+
!Object.hasOwn(descriptor, 'value') ||
|
|
2169
|
+
descriptor.enumerable !== true ||
|
|
2170
|
+
descriptor.writable !== false ||
|
|
2171
|
+
descriptor.configurable !== false) {
|
|
2172
|
+
return { historicalFallback: false, invalidCompatibility: true };
|
|
2173
|
+
}
|
|
2174
|
+
const compat = descriptor.value;
|
|
2175
|
+
if (typeof compat !== 'object' ||
|
|
2176
|
+
compat === null ||
|
|
2177
|
+
Array.isArray(compat) ||
|
|
2178
|
+
Object.getPrototypeOf(compat) !== Object.prototype ||
|
|
2179
|
+
!Object.isFrozen(compat) ||
|
|
2180
|
+
Object.getOwnPropertySymbols(compat).length !== 0) {
|
|
2181
|
+
return { historicalFallback: false, invalidCompatibility: true };
|
|
2182
|
+
}
|
|
2183
|
+
const names = Object.getOwnPropertyNames(compat);
|
|
2184
|
+
const artifactSchema = Object.getOwnPropertyDescriptor(compat, 'artifactSchema');
|
|
2185
|
+
const runtimeAbi = Object.getOwnPropertyDescriptor(compat, 'runtimeAbi');
|
|
2186
|
+
const exact = names.length === 2 &&
|
|
2187
|
+
names.includes('artifactSchema') &&
|
|
2188
|
+
names.includes('runtimeAbi') &&
|
|
2189
|
+
artifactSchema?.enumerable === true &&
|
|
2190
|
+
Object.hasOwn(artifactSchema, 'value') &&
|
|
2191
|
+
artifactSchema.value === 3 &&
|
|
2192
|
+
runtimeAbi?.enumerable === true &&
|
|
2193
|
+
Object.hasOwn(runtimeAbi, 'value') &&
|
|
2194
|
+
runtimeAbi.value === 1;
|
|
2195
|
+
return exact
|
|
2196
|
+
? { schema: 3, historicalFallback: false, invalidCompatibility: false }
|
|
2197
|
+
: { historicalFallback: false, invalidCompatibility: true };
|
|
2198
|
+
}
|
|
2199
|
+
/** Schema decision shared by generated and standalone artifact verification. */
|
|
2200
|
+
export function resolveArtifactSchemaForVerification(opts) {
|
|
2201
|
+
const candidates = [];
|
|
2202
|
+
const invalidSignalFindings = [];
|
|
2203
|
+
if (opts.artifactSchema !== undefined) {
|
|
2204
|
+
candidates.push({
|
|
2205
|
+
source: 'review-supplied artifact schema',
|
|
2206
|
+
schema: opts.artifactSchema,
|
|
2207
|
+
});
|
|
2208
|
+
}
|
|
2209
|
+
const provenanceSchema = artifactSchemaForPlaybookProvenance(opts.provenance);
|
|
2210
|
+
if (opts.provenance !== undefined && provenanceSchema === undefined) {
|
|
2211
|
+
invalidSignalFindings.push(`artifact schema has unsupported link-target provenance ${JSON.stringify(opts.provenance)}`);
|
|
2212
|
+
}
|
|
2213
|
+
if (provenanceSchema !== undefined) {
|
|
2214
|
+
candidates.push({
|
|
2215
|
+
source: 'reviewed link-target provenance',
|
|
2216
|
+
schema: provenanceSchema,
|
|
2217
|
+
});
|
|
2218
|
+
}
|
|
2219
|
+
if (opts.config !== undefined) {
|
|
2220
|
+
const configSignals = promptArtifactSchemaSignalsFromConfig(opts.config);
|
|
2221
|
+
if (configSignals.schema1)
|
|
2222
|
+
candidates.push({ source: 'FSM historical-player structure', schema: 1 });
|
|
2223
|
+
if (configSignals.schema3)
|
|
2224
|
+
candidates.push({ source: 'FSM role/controller structure', schema: 3 });
|
|
2225
|
+
}
|
|
2226
|
+
const linkedSignal = opts.linked === undefined
|
|
2227
|
+
? undefined
|
|
2228
|
+
: linkedArtifactSchemaSignal(opts.linked);
|
|
2229
|
+
if (linkedSignal?.schema !== undefined) {
|
|
2230
|
+
candidates.push({
|
|
2231
|
+
source: 'linked factory compatibility',
|
|
2232
|
+
schema: linkedSignal.schema,
|
|
2233
|
+
});
|
|
2234
|
+
}
|
|
2235
|
+
if (linkedSignal?.invalidCompatibility) {
|
|
2236
|
+
invalidSignalFindings.push('linked factory has an own compatibility declaration that is not exact immutable schema 3/runtime ABI 1');
|
|
2237
|
+
}
|
|
2238
|
+
if (invalidSignalFindings.length > 0) {
|
|
2239
|
+
return { findings: invalidSignalFindings };
|
|
2240
|
+
}
|
|
2241
|
+
const schemas = new Set(candidates.map(({ schema }) => schema));
|
|
2242
|
+
if (schemas.size > 1) {
|
|
2243
|
+
return {
|
|
2244
|
+
findings: [
|
|
2245
|
+
`artifact schema signals disagree (${candidates
|
|
2246
|
+
.map(({ source, schema }) => `${source}: ${schema}`)
|
|
2247
|
+
.join(', ')})`,
|
|
2248
|
+
],
|
|
2249
|
+
};
|
|
2250
|
+
}
|
|
2251
|
+
const [artifactSchema] = schemas;
|
|
2252
|
+
if (artifactSchema !== undefined) {
|
|
2253
|
+
return { artifactSchema, findings: [] };
|
|
2254
|
+
}
|
|
2255
|
+
if (linkedSignal?.historicalFallback) {
|
|
2256
|
+
return { artifactSchema: 1, findings: [] };
|
|
2257
|
+
}
|
|
2258
|
+
const hasAmbiguousCaptainContinuation = opts.config !== undefined &&
|
|
2259
|
+
enumerateCaptainStates(opts.config).some((state) => state.actor === 'captain' &&
|
|
2260
|
+
Object.hasOwn(state.result, NEEDS_BOSS_REPLY));
|
|
2261
|
+
return hasAmbiguousCaptainContinuation
|
|
2262
|
+
? {
|
|
2263
|
+
findings: [
|
|
2264
|
+
'artifact schema has no reviewed provenance, generation-specific actor structure, or callable linked factory for a direct-Captain continuation',
|
|
2265
|
+
],
|
|
2266
|
+
}
|
|
2267
|
+
: { findings: [] };
|
|
2268
|
+
}
|
|
1695
2269
|
/**
|
|
1696
2270
|
* Emits the prompt-contract test beside a compiled `playbook` artifact
|
|
1697
|
-
* (
|
|
2271
|
+
* (verification-5): derives and pins the per-state contract from the physical
|
|
1698
2272
|
* `<basename>.fsm.ts` artifact, then emits NodeNext `.js` imports for that FSM
|
|
1699
2273
|
* and any linked `<basename>.playbook.ts` module. When the linked module
|
|
1700
2274
|
* exposes the `_internal` composer matching each state actor —
|
|
@@ -1711,6 +2285,14 @@ export async function emitPromptContractTest(opts) {
|
|
|
1711
2285
|
const fsmPath = join(opts.artifactDir, `${opts.basename}.fsm.ts`);
|
|
1712
2286
|
const config = findMachineConfig(await loadFsmModule(fsmPath));
|
|
1713
2287
|
const rows = capturePromptContract(config);
|
|
2288
|
+
let schemaResolution = resolveArtifactSchemaForVerification({
|
|
2289
|
+
config,
|
|
2290
|
+
...(opts.provenance === undefined ? {} : { provenance: opts.provenance }),
|
|
2291
|
+
...(opts.artifactSchema === undefined
|
|
2292
|
+
? {}
|
|
2293
|
+
: { artifactSchema: opts.artifactSchema }),
|
|
2294
|
+
});
|
|
2295
|
+
let artifactSchema = schemaResolution.artifactSchema;
|
|
1714
2296
|
let composer;
|
|
1715
2297
|
const linkedPath = join(opts.artifactDir, `${opts.basename}.playbook.ts`);
|
|
1716
2298
|
if (existsSync(linkedPath)) {
|
|
@@ -1719,6 +2301,17 @@ export async function emitPromptContractTest(opts) {
|
|
|
1719
2301
|
linkedPath,
|
|
1720
2302
|
fsmPath,
|
|
1721
2303
|
}));
|
|
2304
|
+
schemaResolution = resolveArtifactSchemaForVerification({
|
|
2305
|
+
config,
|
|
2306
|
+
linked,
|
|
2307
|
+
...(opts.provenance === undefined
|
|
2308
|
+
? {}
|
|
2309
|
+
: { provenance: opts.provenance }),
|
|
2310
|
+
...(opts.artifactSchema === undefined
|
|
2311
|
+
? {}
|
|
2312
|
+
: { artifactSchema: opts.artifactSchema }),
|
|
2313
|
+
});
|
|
2314
|
+
artifactSchema = schemaResolution.artifactSchema;
|
|
1722
2315
|
const actors = new Set(enumerateCaptainStates(config).map(({ actor }) => actor));
|
|
1723
2316
|
const substitutions = {};
|
|
1724
2317
|
for (const actor of ['captain', 'player']) {
|
|
@@ -1736,6 +2329,7 @@ export async function emitPromptContractTest(opts) {
|
|
|
1736
2329
|
config,
|
|
1737
2330
|
compose: typedCompose,
|
|
1738
2331
|
actor,
|
|
2332
|
+
...(artifactSchema === undefined ? {} : { artifactSchema }),
|
|
1739
2333
|
});
|
|
1740
2334
|
diagnostics.push(...findings.map((finding) => `prompt contract: ${finding}`));
|
|
1741
2335
|
}
|
|
@@ -1751,11 +2345,14 @@ export async function emitPromptContractTest(opts) {
|
|
|
1751
2345
|
diagnostics.push(`prompt contract: linked module could not be imported (${messageOf(error)}); composition checks not emitted`);
|
|
1752
2346
|
}
|
|
1753
2347
|
}
|
|
2348
|
+
diagnostics.unshift(...schemaResolution.findings.map((finding) => `prompt contract: ${finding}`));
|
|
1754
2349
|
const content = generatePromptContractTest({
|
|
1755
2350
|
basename: opts.basename,
|
|
1756
2351
|
fsmModule: `./${opts.basename}.fsm.js`,
|
|
1757
2352
|
verifyModule: opts.verifyModule ?? VERIFY_MODULE,
|
|
1758
2353
|
rows,
|
|
2354
|
+
...(artifactSchema === undefined ? {} : { artifactSchema }),
|
|
2355
|
+
schemaFindings: schemaResolution.findings,
|
|
1759
2356
|
composer,
|
|
1760
2357
|
});
|
|
1761
2358
|
await mkdir(opts.artifactDir, { recursive: true });
|
|
@@ -1765,7 +2362,7 @@ export async function emitPromptContractTest(opts) {
|
|
|
1765
2362
|
}
|
|
1766
2363
|
/**
|
|
1767
2364
|
* Emits the introspection test beside a compiled `playbook` artifact
|
|
1768
|
-
* (
|
|
2365
|
+
* (verification-4): derives topology pins from the physical `<basename>.fsm.ts`,
|
|
1769
2366
|
* emits a NodeNext `.js` import for that sibling source, and writes
|
|
1770
2367
|
* `<basename>.fsm.introspect.test.ts` into the artifact directory.
|
|
1771
2368
|
*
|
|
@@ -1785,7 +2382,7 @@ export async function emitFsmIntrospectionTest(opts) {
|
|
|
1785
2382
|
await writeFile(path, content);
|
|
1786
2383
|
return path;
|
|
1787
2384
|
}
|
|
1788
|
-
// Transition-coverage verification (
|
|
2385
|
+
// Transition-coverage verification (verification-6) lives in its own module — it
|
|
1789
2386
|
// depends on `xstate` to drive the machine — and is re-exported here so every
|
|
1790
2387
|
// generated test imports one checker module (`@sublang/slc/verify`).
|
|
1791
2388
|
export * from './verify-coverage.js';
|