@sublang/slc 0.5.0 → 0.7.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 +10 -5
- package/dist/app.d.ts +20 -20
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +44 -31
- 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 +9 -2
- package/dist/cligent-agent.d.ts.map +1 -1
- package/dist/cligent-agent.js +6 -5
- package/dist/cligent-agent.js.map +1 -1
- package/dist/compiled-execution.d.ts +44 -0
- package/dist/compiled-execution.d.ts.map +1 -0
- package/dist/compiled-execution.js +184 -0
- package/dist/compiled-execution.js.map +1 -0
- package/dist/compiled-executor.d.ts +16 -6
- package/dist/compiled-executor.d.ts.map +1 -1
- package/dist/compiled-executor.js +182 -35
- package/dist/compiled-executor.js.map +1 -1
- package/dist/config-file.d.ts +9 -6
- package/dist/config-file.d.ts.map +1 -1
- package/dist/config-file.js +27 -9
- package/dist/config-file.js.map +1 -1
- package/dist/config.d.ts +77 -24
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +128 -52
- 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/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 +13 -6
- package/dist/phase-runner.d.ts.map +1 -1
- package/dist/phase-runner.js +12 -10
- package/dist/phase-runner.js.map +1 -1
- 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 +24 -10
- 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 +49 -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 +2 -2
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +218 -81
- package/dist/runner.js.map +1 -1
- package/dist/runtime-contract.d.ts +17 -0
- package/dist/runtime-contract.d.ts.map +1 -0
- package/dist/runtime-contract.js +114 -0
- package/dist/runtime-contract.js.map +1 -0
- package/dist/slc.config.template.yaml +7 -2
- 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 +153 -15
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js +707 -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,83 @@ 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
|
+
/**
|
|
36
|
+
* True when the declaration admits the roleless schema-3 `composed-v3`
|
|
37
|
+
* generation: `RUNTIME_ABI` is exactly `1` and `SUPPORTED_ARTIFACT_SCHEMAS`
|
|
38
|
+
* contains `3` (DR-028).
|
|
39
|
+
*/
|
|
40
|
+
export function declaresComposedV3(declaration) {
|
|
41
|
+
return (declaration.runtimeAbi === 1 &&
|
|
42
|
+
Array.isArray(declaration.supportedArtifactSchemas) &&
|
|
43
|
+
declaration.supportedArtifactSchemas.includes(3));
|
|
44
|
+
}
|
|
45
|
+
/** Names a declaration for fail-closed diagnostics. */
|
|
46
|
+
export function describeRuntimeDeclaration(declaration) {
|
|
47
|
+
const abi = declaration.runtimeAbi === undefined
|
|
48
|
+
? 'no RUNTIME_ABI'
|
|
49
|
+
: `RUNTIME_ABI ${renderDeclared(declaration.runtimeAbi)}`;
|
|
50
|
+
const schemas = declaration.supportedArtifactSchemas === undefined
|
|
51
|
+
? 'no SUPPORTED_ARTIFACT_SCHEMAS'
|
|
52
|
+
: `SUPPORTED_ARTIFACT_SCHEMAS ${renderDeclared(declaration.supportedArtifactSchemas)}`;
|
|
53
|
+
return `${declaration.provenance} declares ${abi} and ${schemas}`;
|
|
54
|
+
}
|
|
55
|
+
function renderDeclared(value) {
|
|
56
|
+
try {
|
|
57
|
+
const json = JSON.stringify(value);
|
|
58
|
+
return json === undefined ? String(value) : json;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return String(value);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Artifact schema recorded for an exact reviewed Playbook provenance: the
|
|
66
|
+
* historical map kept as recorded (DR-028). A later release supplies its
|
|
67
|
+
* schema through the installed engine's declaration instead
|
|
68
|
+
* ({@link resolveArtifactSchemaForVerification}).
|
|
69
|
+
*/
|
|
70
|
+
export function artifactSchemaForPlaybookProvenance(provenance) {
|
|
71
|
+
switch (provenance) {
|
|
72
|
+
case '@sublang/playbook@0.10.0':
|
|
73
|
+
case '@sublang/playbook@1.0.0':
|
|
74
|
+
case '@sublang/playbook@2.0.0':
|
|
75
|
+
case '@sublang/playbook@3.1.0':
|
|
76
|
+
case '@sublang/playbook@4.0.0':
|
|
77
|
+
return 1;
|
|
78
|
+
case '@sublang/playbook@10.0.0':
|
|
79
|
+
return 3;
|
|
80
|
+
default:
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Returns the Playbook package provenance that owns an invocation's concrete
|
|
86
|
+
* link target. The first package manifest above the target owns the file; a
|
|
87
|
+
* parent workspace manifest must not lend its identity to a nested local file.
|
|
88
|
+
*/
|
|
89
|
+
export async function playbookProvenanceForLinkTarget(linkTarget) {
|
|
90
|
+
let cursor = dirname(resolve(linkTarget));
|
|
91
|
+
for (;;) {
|
|
92
|
+
const manifestPath = join(cursor, 'package.json');
|
|
93
|
+
if (existsSync(manifestPath)) {
|
|
94
|
+
try {
|
|
95
|
+
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'));
|
|
96
|
+
return manifest.name === '@sublang/playbook' &&
|
|
97
|
+
typeof manifest.version === 'string' &&
|
|
98
|
+
manifest.version.length > 0
|
|
99
|
+
? `${manifest.name}@${manifest.version}`
|
|
100
|
+
: undefined;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const parent = dirname(cursor);
|
|
107
|
+
if (parent === cursor)
|
|
108
|
+
return undefined;
|
|
109
|
+
cursor = parent;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
35
112
|
const ITEM_HEADING = /^###\s+([A-Za-z][\w-]*)\s*$/;
|
|
36
113
|
// The `text2gears` item form names a delegated player as "Captain shall prompt
|
|
37
114
|
// <Player>" (or a "relay ... to <Player>" variant); English players are
|
|
@@ -41,6 +118,7 @@ const ITEM_HEADING = /^###\s+([A-Za-z][\w-]*)\s*$/;
|
|
|
41
118
|
const ITEM_PLAYER = /Captain shall (?:prompt|relay\b[^.]*?\bto)\s+(?:`([^`]+)`|"([^"]+)"|“([^”]+)”|([A-Z][\w]*)|([^\p{ASCII}][^\s::,,。;;]*))/u;
|
|
42
119
|
const ITEM_PLAYBOOK = /Captain shall call playbook\s+(?:`([^`]+)`|"([^"]+)"|“([^”]+)”|([A-Za-z0-9][\w.-]*))\s*:/;
|
|
43
120
|
const ITEM_DYNAMIC_PLAYBOOK = /Captain shall call playbook selected by\s+`([^`]+)`\s*:/;
|
|
121
|
+
const PARALLEL_GROUP = /^Parallel group:\s*(\S(?:.*\S)?)\s*$/;
|
|
44
122
|
const DYNAMIC_TEXT = /^<([A-Za-z_$][A-Za-z0-9_$]*)>$/;
|
|
45
123
|
// An optimizer-introduced script item runs a shell command without any agent
|
|
46
124
|
// (text2gears.md "Script behaviors"; DR-013). The clause is fixed machine
|
|
@@ -112,6 +190,9 @@ export function parseGearsItems(gears) {
|
|
|
112
190
|
...(dynamicText === null ? {} : { textContext: dynamicText[1] }),
|
|
113
191
|
}
|
|
114
192
|
: {}),
|
|
193
|
+
...(current.parallelGroup !== ''
|
|
194
|
+
? { parallelGroup: current.parallelGroup }
|
|
195
|
+
: {}),
|
|
115
196
|
...(current.resultDeclared
|
|
116
197
|
? { result: Object.fromEntries(current.results) }
|
|
117
198
|
: {}),
|
|
@@ -133,6 +214,7 @@ export function parseGearsItems(gears) {
|
|
|
133
214
|
script: false,
|
|
134
215
|
playbookId: '',
|
|
135
216
|
playbookIdContext: '',
|
|
217
|
+
parallelGroup: '',
|
|
136
218
|
prompt: [],
|
|
137
219
|
resultsEligible: false,
|
|
138
220
|
resultDeclared: false,
|
|
@@ -195,6 +277,10 @@ export function parseGearsItems(gears) {
|
|
|
195
277
|
}
|
|
196
278
|
if (line.trim() !== '')
|
|
197
279
|
current.resultsEligible = false;
|
|
280
|
+
const parallelGroup = PARALLEL_GROUP.exec(line.trim());
|
|
281
|
+
if (parallelGroup !== null && current.parallelGroup === '') {
|
|
282
|
+
current.parallelGroup = parallelGroup[1];
|
|
283
|
+
}
|
|
198
284
|
const player = ITEM_PLAYER.exec(line);
|
|
199
285
|
if (player !== null && current.player === '') {
|
|
200
286
|
current.player =
|
|
@@ -221,6 +307,152 @@ export function parseGearsItems(gears) {
|
|
|
221
307
|
flush();
|
|
222
308
|
return items;
|
|
223
309
|
}
|
|
310
|
+
const ROLE_DECLARATION = /^(?:#{1,6}\s+(Roles|Players)|(Roles|Players):)\s*$/;
|
|
311
|
+
/** Canonical lowercase local-role id used by schema-3 artifacts. */
|
|
312
|
+
export function canonicalRoleId(name) {
|
|
313
|
+
return name.toLowerCase();
|
|
314
|
+
}
|
|
315
|
+
function declarationName(value) {
|
|
316
|
+
const match = /^[`"“]?([^`"”]+?)[`"”]?\s*$/.exec(value.trim());
|
|
317
|
+
return match?.[1].trim() || undefined;
|
|
318
|
+
}
|
|
319
|
+
/** Parses Roles/Players plus source-derived concurrent role sets without host bindings. */
|
|
320
|
+
export function inspectGearsRoleContract(gears) {
|
|
321
|
+
const findings = [];
|
|
322
|
+
const declarations = [];
|
|
323
|
+
let active;
|
|
324
|
+
for (const line of gears.split('\n')) {
|
|
325
|
+
const heading = ROLE_DECLARATION.exec(line.trim());
|
|
326
|
+
if (heading !== null) {
|
|
327
|
+
active = {
|
|
328
|
+
kind: (heading[1] ?? heading[2]),
|
|
329
|
+
names: [],
|
|
330
|
+
};
|
|
331
|
+
declarations.push(active);
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
if (active === undefined)
|
|
335
|
+
continue;
|
|
336
|
+
if (line.trim() === '')
|
|
337
|
+
continue;
|
|
338
|
+
const bullet = /^-\s+(.*)$/.exec(line.trim());
|
|
339
|
+
if (bullet === null) {
|
|
340
|
+
active = undefined;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
const declaration = bullet[1].trim();
|
|
344
|
+
if (active.kind === 'Roles' && /[=|]/.test(declaration)) {
|
|
345
|
+
findings.push(`Roles declaration ${JSON.stringify(declaration)} uses removed alias syntax`);
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
// Historical schema-1 Players may declare a composite launcher choice
|
|
349
|
+
// (`Committer = Coder | Reviewer`). It is not one concrete player binding
|
|
350
|
+
// and therefore does not enter the source-order player list, including
|
|
351
|
+
// when every name is backtick- or quote-delimited.
|
|
352
|
+
if (active.kind === 'Players' && declaration.includes('='))
|
|
353
|
+
continue;
|
|
354
|
+
const name = declarationName(declaration);
|
|
355
|
+
if (name === undefined) {
|
|
356
|
+
findings.push(`malformed ${active.kind} declaration ${JSON.stringify(declaration)}`);
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
active.names.push(name);
|
|
360
|
+
}
|
|
361
|
+
const kinds = new Set(declarations.map(({ kind }) => kind));
|
|
362
|
+
if (declarations.length > 1) {
|
|
363
|
+
findings.push('GEARS declares more than one Roles/Players section');
|
|
364
|
+
}
|
|
365
|
+
if (kinds.size > 1) {
|
|
366
|
+
findings.push('GEARS mixes Roles and Players declarations');
|
|
367
|
+
}
|
|
368
|
+
const selected = declarations[0];
|
|
369
|
+
const generation = selected?.kind === 'Roles'
|
|
370
|
+
? 'schema-3'
|
|
371
|
+
: selected?.kind === 'Players'
|
|
372
|
+
? 'schema-1'
|
|
373
|
+
: 'unspecified';
|
|
374
|
+
const names = selected?.names ?? [];
|
|
375
|
+
const roleIds = names.map(canonicalRoleId);
|
|
376
|
+
if (generation === 'schema-3') {
|
|
377
|
+
const byCanonical = new Map();
|
|
378
|
+
for (let index = 0; index < names.length; index += 1) {
|
|
379
|
+
const name = names[index];
|
|
380
|
+
const roleId = roleIds[index];
|
|
381
|
+
const existing = byCanonical.get(roleId);
|
|
382
|
+
if (existing !== undefined) {
|
|
383
|
+
findings.push(existing === name
|
|
384
|
+
? `Roles declaration repeats ${JSON.stringify(name)}`
|
|
385
|
+
: `Roles declarations ${JSON.stringify(existing)} and ${JSON.stringify(name)} collide as canonical role ${JSON.stringify(roleId)}`);
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
byCanonical.set(roleId, name);
|
|
389
|
+
}
|
|
390
|
+
// A canonical local role id is the declared name lowercased. Playbook 10
|
|
391
|
+
// explicitly admits non-English role names ("quote non-English names
|
|
392
|
+
// (e.g., `作者`)"), and a script without case - Chinese among them -
|
|
393
|
+
// lowercases to itself, so an ASCII-only class would reject exactly the
|
|
394
|
+
// names the definition sanctions. Require instead that the id be genuinely
|
|
395
|
+
// canonical: already lowercase, carrying no whitespace or separator that
|
|
396
|
+
// would make it ambiguous as an identifier.
|
|
397
|
+
if (roleId.length === 0 ||
|
|
398
|
+
roleId !== roleId.toLowerCase() ||
|
|
399
|
+
/[\s.,;:/\\'"`()[\]{}<>|]/u.test(roleId)) {
|
|
400
|
+
findings.push(`Roles declaration ${JSON.stringify(name)} derives noncanonical local role ${JSON.stringify(roleId)}`);
|
|
401
|
+
}
|
|
402
|
+
else if (roleId === 'captain') {
|
|
403
|
+
findings.push('Roles declaration uses reserved local role "captain"');
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
const groups = new Map();
|
|
408
|
+
if (generation === 'schema-3') {
|
|
409
|
+
const declared = new Set(roleIds);
|
|
410
|
+
for (const item of parseGearsItems(gears)) {
|
|
411
|
+
if (item.actor === 'player') {
|
|
412
|
+
const roleId = canonicalRoleId(item.player);
|
|
413
|
+
if (!declared.has(roleId)) {
|
|
414
|
+
findings.push(`GEARS item ${item.id} delegates to undeclared role ${JSON.stringify(item.player)}`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
if (item.parallelGroup === undefined)
|
|
418
|
+
continue;
|
|
419
|
+
if (item.actor !== 'player') {
|
|
420
|
+
findings.push(`parallel group ${JSON.stringify(item.parallelGroup)} contains non-role item ${item.id}`);
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
const roleId = canonicalRoleId(item.player);
|
|
424
|
+
const members = groups.get(item.parallelGroup) ?? [];
|
|
425
|
+
if (members.includes(roleId)) {
|
|
426
|
+
findings.push(`parallel group ${JSON.stringify(item.parallelGroup)} repeats canonical role ${JSON.stringify(roleId)}`);
|
|
427
|
+
}
|
|
428
|
+
members.push(roleId);
|
|
429
|
+
groups.set(item.parallelGroup, members);
|
|
430
|
+
}
|
|
431
|
+
const groupByMembers = new Map();
|
|
432
|
+
for (const [group, members] of groups) {
|
|
433
|
+
if (members.length < 2) {
|
|
434
|
+
findings.push(`parallel group ${JSON.stringify(group)} contains fewer than two roles`);
|
|
435
|
+
}
|
|
436
|
+
// A concurrent role set is unordered for duplicate detection even
|
|
437
|
+
// though its source member order remains significant in the FSM export.
|
|
438
|
+
const signature = JSON.stringify([...members].sort());
|
|
439
|
+
const existing = groupByMembers.get(signature);
|
|
440
|
+
if (existing !== undefined) {
|
|
441
|
+
findings.push(`parallel groups ${JSON.stringify(existing)} and ${JSON.stringify(group)} duplicate concurrent role set ${signature}`);
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
groupByMembers.set(signature, group);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return {
|
|
449
|
+
generation,
|
|
450
|
+
names,
|
|
451
|
+
roleIds,
|
|
452
|
+
concurrentRoleSets: [...groups.values()],
|
|
453
|
+
findings,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
224
456
|
/** Walks every state node depth-first in declaration order. */
|
|
225
457
|
function walkStateNodes(config) {
|
|
226
458
|
const out = [];
|
|
@@ -277,6 +509,14 @@ function metadataStateId(state) {
|
|
|
277
509
|
const stateId = playbook.stateId;
|
|
278
510
|
return isNonEmptyString(stateId) ? stateId : undefined;
|
|
279
511
|
}
|
|
512
|
+
function metadataRole(state) {
|
|
513
|
+
if (typeof state.meta !== 'object' || state.meta === null)
|
|
514
|
+
return undefined;
|
|
515
|
+
const playbook = state.meta.playbook;
|
|
516
|
+
if (typeof playbook !== 'object' || playbook === null)
|
|
517
|
+
return undefined;
|
|
518
|
+
return playbook.role;
|
|
519
|
+
}
|
|
280
520
|
function stateIdConsistencyFindings(node, inputStateId) {
|
|
281
521
|
if (!isNonEmptyString(inputStateId))
|
|
282
522
|
return [];
|
|
@@ -359,11 +599,11 @@ function enumerateCaptainBindings(config) {
|
|
|
359
599
|
(invoke.src !== undefined || !isNonEmptyString(fields.sourceItem))) {
|
|
360
600
|
continue;
|
|
361
601
|
}
|
|
362
|
-
// Published artifacts
|
|
363
|
-
//
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
602
|
+
// Published schema-1 artifacts carried a concrete player field. Schema 3
|
|
603
|
+
// carries only its canonical local role and repeats it in public metadata.
|
|
604
|
+
const actor = source === 'player' ||
|
|
605
|
+
Object.hasOwn(fields, 'player') ||
|
|
606
|
+
Object.hasOwn(fields, 'role')
|
|
367
607
|
? 'player'
|
|
368
608
|
: 'captain';
|
|
369
609
|
const pinActor = source === 'player' || (source === 'captain' && actor === 'captain');
|
|
@@ -371,8 +611,37 @@ function enumerateCaptainBindings(config) {
|
|
|
371
611
|
if (!isNonEmptyString(fields.sourceItem)) {
|
|
372
612
|
bindingFindings.push('invoke.input.sourceItem is not a non-empty string');
|
|
373
613
|
}
|
|
374
|
-
if (actor === 'player'
|
|
375
|
-
|
|
614
|
+
if (actor === 'player') {
|
|
615
|
+
const hasPlayer = Object.hasOwn(fields, 'player');
|
|
616
|
+
const hasRole = Object.hasOwn(fields, 'role');
|
|
617
|
+
if (hasPlayer && hasRole) {
|
|
618
|
+
bindingFindings.push('invoke.input carries both historical player and schema-3 role');
|
|
619
|
+
}
|
|
620
|
+
if (hasRole && typeof fields.role !== 'string') {
|
|
621
|
+
bindingFindings.push('invoke.input.role is not a string');
|
|
622
|
+
}
|
|
623
|
+
if (hasRole && source !== 'player') {
|
|
624
|
+
bindingFindings.push('schema-3 delegated work does not invoke the player actor');
|
|
625
|
+
}
|
|
626
|
+
if (!hasRole && typeof fields.player !== 'string') {
|
|
627
|
+
bindingFindings.push('invoke.input.player is not a string');
|
|
628
|
+
}
|
|
629
|
+
const publicRole = metadataRole(node.state);
|
|
630
|
+
if (hasRole) {
|
|
631
|
+
if (typeof publicRole !== 'string') {
|
|
632
|
+
bindingFindings.push('state.meta.playbook.role is not a string for schema-3 delegated work');
|
|
633
|
+
}
|
|
634
|
+
else if (publicRole !== fields.role) {
|
|
635
|
+
bindingFindings.push(`state.meta.playbook.role ${JSON.stringify(publicRole)} does not match invoke.input.role ${JSON.stringify(fields.role)}`);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
else if (publicRole !== undefined) {
|
|
639
|
+
bindingFindings.push('historical delegated-player state unexpectedly declares state.meta.playbook.role');
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
else if (Object.hasOwn(fields, 'role') ||
|
|
643
|
+
metadataRole(node.state) !== undefined) {
|
|
644
|
+
bindingFindings.push('direct-Captain state unexpectedly declares a role binding');
|
|
376
645
|
}
|
|
377
646
|
if (typeof fields.prompt !== 'string') {
|
|
378
647
|
bindingFindings.push('invoke.input.prompt is not a string');
|
|
@@ -395,6 +664,7 @@ function enumerateCaptainBindings(config) {
|
|
|
395
664
|
: '',
|
|
396
665
|
actor,
|
|
397
666
|
player: typeof fields.player === 'string' ? fields.player : '',
|
|
667
|
+
...(typeof fields.role === 'string' ? { role: fields.role } : {}),
|
|
398
668
|
prompt: typeof fields.prompt === 'string' ? fields.prompt : '',
|
|
399
669
|
result: resultMap(fields.result),
|
|
400
670
|
...nestedStatePath(node),
|
|
@@ -655,24 +925,115 @@ function statePlaybookSignature(state) {
|
|
|
655
925
|
state.textContext ?? null,
|
|
656
926
|
]);
|
|
657
927
|
}
|
|
928
|
+
/** Exact action guards of Playbook 10's controller decision result. */
|
|
929
|
+
export const CONTROLLER_ACTION_GUARDS = [
|
|
930
|
+
'respond',
|
|
931
|
+
'resume',
|
|
932
|
+
'start',
|
|
933
|
+
'switch',
|
|
934
|
+
'dismiss',
|
|
935
|
+
'deliver',
|
|
936
|
+
'runtime',
|
|
937
|
+
];
|
|
938
|
+
/** Whether a result map has the exact Playbook 10 controller domain union. */
|
|
939
|
+
export function isControllerDecisionResult(result) {
|
|
940
|
+
if (!isStringMap(result))
|
|
941
|
+
return false;
|
|
942
|
+
const keys = Object.keys(result).filter((key) => key !== NEEDS_BOSS_REPLY);
|
|
943
|
+
return (keys.length === CONTROLLER_ACTION_GUARDS.length &&
|
|
944
|
+
CONTROLLER_ACTION_GUARDS.every((guard) => Object.hasOwn(result, guard)));
|
|
945
|
+
}
|
|
946
|
+
/** A single missing or extra key against Playbook 10's controller domain. */
|
|
947
|
+
export function controllerDecisionNearMiss(result) {
|
|
948
|
+
if (!isStringMap(result))
|
|
949
|
+
return undefined;
|
|
950
|
+
const actual = Object.keys(result).filter((key) => key !== NEEDS_BOSS_REPLY);
|
|
951
|
+
const expected = new Set(CONTROLLER_ACTION_GUARDS);
|
|
952
|
+
const present = new Set(actual);
|
|
953
|
+
const missing = CONTROLLER_ACTION_GUARDS.filter((key) => !present.has(key));
|
|
954
|
+
const extra = actual.filter((key) => !expected.has(key));
|
|
955
|
+
return missing.length + extra.length === 1 ? { missing, extra } : undefined;
|
|
956
|
+
}
|
|
957
|
+
/** Whether a machine contains Playbook 10's grounded controller decision state. */
|
|
958
|
+
export function isControllerMachine(config) {
|
|
959
|
+
return enumerateCaptainBindings(config).some(({ state, pinActor }) => {
|
|
960
|
+
if (!pinActor || state.actor !== 'captain')
|
|
961
|
+
return false;
|
|
962
|
+
if (state.bindingFindings?.includes('invoke.input.result is not a string-valued object')) {
|
|
963
|
+
return false;
|
|
964
|
+
}
|
|
965
|
+
return isControllerDecisionResult(state.result);
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
/** Whether an explicit direct-Captain result is one key from the controller domain. */
|
|
969
|
+
export function hasControllerDecisionNearMiss(config) {
|
|
970
|
+
return enumerateCaptainBindings(config).some(({ state, pinActor }) => pinActor &&
|
|
971
|
+
state.actor === 'captain' &&
|
|
972
|
+
state.result[NEEDS_BOSS_REPLY] === undefined &&
|
|
973
|
+
controllerDecisionNearMiss(state.result) !== undefined);
|
|
974
|
+
}
|
|
975
|
+
function concurrentRoleSets(value) {
|
|
976
|
+
if (!Array.isArray(value))
|
|
977
|
+
return undefined;
|
|
978
|
+
const sets = [];
|
|
979
|
+
for (const candidate of value) {
|
|
980
|
+
if (!Array.isArray(candidate) ||
|
|
981
|
+
candidate.some((role) => typeof role !== 'string')) {
|
|
982
|
+
return undefined;
|
|
983
|
+
}
|
|
984
|
+
sets.push([...candidate]);
|
|
985
|
+
}
|
|
986
|
+
return sets;
|
|
987
|
+
}
|
|
658
988
|
/**
|
|
659
989
|
* Checks GEARS↔FSM conformance and returns human-readable findings (empty when
|
|
660
990
|
* conformant): every GEARS item maps to one state with the same player and the
|
|
661
991
|
* prompt verbatim, every captain state references a known item, and every
|
|
662
992
|
* captain state's `result` map declares the Boss-reply suspension key with its
|
|
663
|
-
* adjudicator contract (
|
|
993
|
+
* adjudicator contract (verification-1, verification-3; DR-009).
|
|
664
994
|
*/
|
|
665
|
-
export function checkGearsFsmConformance(gears, config) {
|
|
995
|
+
export function checkGearsFsmConformance(gears, config, options = {}) {
|
|
666
996
|
const items = parseGearsItems(gears);
|
|
997
|
+
const roleContract = inspectGearsRoleContract(gears);
|
|
998
|
+
const controller = isControllerMachine(config);
|
|
667
999
|
const captainBindings = enumerateCaptainBindings(config);
|
|
668
1000
|
const states = captainBindings.map(({ state }) => state);
|
|
1001
|
+
const controllerNearMisses = captainBindings.flatMap(({ state, pinActor }) => {
|
|
1002
|
+
if (!pinActor ||
|
|
1003
|
+
state.actor !== 'captain' ||
|
|
1004
|
+
state.result[NEEDS_BOSS_REPLY] !== undefined) {
|
|
1005
|
+
return [];
|
|
1006
|
+
}
|
|
1007
|
+
const nearMiss = controllerDecisionNearMiss(state.result);
|
|
1008
|
+
return nearMiss === undefined ? [] : [{ state, nearMiss }];
|
|
1009
|
+
});
|
|
1010
|
+
const controllerNearMissStates = new Set(controllerNearMisses.map(({ state }) => state));
|
|
669
1011
|
const explicitActorStates = new Set(captainBindings
|
|
670
1012
|
.filter(({ pinActor }) => pinActor)
|
|
671
1013
|
.map(({ state }) => state));
|
|
672
1014
|
const playbookStates = enumeratePlaybookStates(config);
|
|
673
1015
|
const scriptStates = enumerateScriptStates(config);
|
|
674
1016
|
const findings = [];
|
|
1017
|
+
for (const { state, nearMiss } of controllerNearMisses) {
|
|
1018
|
+
const detail = nearMiss.missing.length > 0
|
|
1019
|
+
? `missing ${JSON.stringify(nearMiss.missing[0])}`
|
|
1020
|
+
: `extra ${JSON.stringify(nearMiss.extra[0])}`;
|
|
1021
|
+
findings.push(`FSM state ${state.stateId}: controller decision contract near-miss (${detail}); the controller domain requires exactly ${CONTROLLER_ACTION_GUARDS.join(', ')}`);
|
|
1022
|
+
}
|
|
675
1023
|
findings.push(...structuredStateIdentityFindings(walkStateNodes(config)));
|
|
1024
|
+
findings.push(...roleContract.findings);
|
|
1025
|
+
const requiresConcurrentRoleSets = roleContract.generation === 'schema-3' ||
|
|
1026
|
+
controller ||
|
|
1027
|
+
options.artifactSchema === 3;
|
|
1028
|
+
if (requiresConcurrentRoleSets) {
|
|
1029
|
+
const actual = concurrentRoleSets(options.concurrentRoleSets);
|
|
1030
|
+
if (actual === undefined) {
|
|
1031
|
+
findings.push('schema-3 FSM exports no valid concurrentRoleSets array');
|
|
1032
|
+
}
|
|
1033
|
+
else if (JSON.stringify(actual) !== JSON.stringify(roleContract.concurrentRoleSets)) {
|
|
1034
|
+
findings.push(`schema-3 FSM concurrentRoleSets ${JSON.stringify(actual)} do not match GEARS groups ${JSON.stringify(roleContract.concurrentRoleSets)}`);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
676
1037
|
for (const item of items) {
|
|
677
1038
|
findings.push(...(item.resultFindings ?? []).map((finding) => `GEARS item ${item.id}: ${finding}`));
|
|
678
1039
|
}
|
|
@@ -840,8 +1201,24 @@ export function checkGearsFsmConformance(gears, config) {
|
|
|
840
1201
|
state.actor !== item.actor) {
|
|
841
1202
|
findings.push(`${item.id}: FSM actor "${state.actor}" is not GEARS actor "${item.actor}"`);
|
|
842
1203
|
}
|
|
843
|
-
if (item.actor === 'player'
|
|
844
|
-
|
|
1204
|
+
if (item.actor === 'player') {
|
|
1205
|
+
if (roleContract.generation === 'schema-3') {
|
|
1206
|
+
const expectedRole = canonicalRoleId(item.player);
|
|
1207
|
+
if (state.role !== expectedRole) {
|
|
1208
|
+
findings.push(`${item.id}: FSM role ${JSON.stringify(state.role ?? '')} is not GEARS canonical role ${JSON.stringify(expectedRole)}`);
|
|
1209
|
+
}
|
|
1210
|
+
if (state.player !== '') {
|
|
1211
|
+
findings.push(`${item.id}: schema-3 delegated role carries removed invoke.input.player ${JSON.stringify(state.player)}`);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
else {
|
|
1215
|
+
if (state.player !== item.player) {
|
|
1216
|
+
findings.push(`${item.id}: FSM player "${state.player}" is not GEARS player "${item.player}"`);
|
|
1217
|
+
}
|
|
1218
|
+
if (state.role !== undefined) {
|
|
1219
|
+
findings.push(`${item.id}: historical delegated player unexpectedly carries invoke.input.role ${JSON.stringify(state.role)}`);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
845
1222
|
}
|
|
846
1223
|
if (state.prompt !== item.prompt) {
|
|
847
1224
|
findings.push(`${item.id}: FSM prompt is not the GEARS prompt verbatim`);
|
|
@@ -871,11 +1248,19 @@ export function checkGearsFsmConformance(gears, config) {
|
|
|
871
1248
|
if (state.sourceItem !== '' && !itemIds.has(state.sourceItem)) {
|
|
872
1249
|
findings.push(`FSM state ${state.stateId} references unknown GEARS item ${state.sourceItem}`);
|
|
873
1250
|
}
|
|
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
1251
|
const bossReply = state.result[NEEDS_BOSS_REPLY];
|
|
878
|
-
if (
|
|
1252
|
+
if (controller) {
|
|
1253
|
+
if (bossReply !== undefined) {
|
|
1254
|
+
findings.push(`FSM controller state ${state.stateId} unexpectedly declares ${NEEDS_BOSS_REPLY}`);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
else if (controllerNearMissStates.has(state)) {
|
|
1258
|
+
// The precise controller-domain diagnostic above owns a malformed
|
|
1259
|
+
// near-controller state; do not suggest adding the ordinary wait key to
|
|
1260
|
+
// that state, while retaining ordinary findings for every other state.
|
|
1261
|
+
continue;
|
|
1262
|
+
}
|
|
1263
|
+
else if (bossReply === undefined) {
|
|
879
1264
|
findings.push(`FSM state ${state.stateId} declares no ${NEEDS_BOSS_REPLY} result`);
|
|
880
1265
|
}
|
|
881
1266
|
else if (!bossReply.includes(BOSS_QUESTION_MARKER)) {
|
|
@@ -898,7 +1283,7 @@ export function checkGearsFsmConformance(gears, config) {
|
|
|
898
1283
|
return findings;
|
|
899
1284
|
}
|
|
900
1285
|
/*
|
|
901
|
-
* Machine introspection (
|
|
1286
|
+
* Machine introspection (verification-4).
|
|
902
1287
|
*
|
|
903
1288
|
* `pinIntrospection` reduces a machine config to its structural facts — the
|
|
904
1289
|
* captain-state bindings, every transition arm, the root and quiescent event
|
|
@@ -963,7 +1348,7 @@ function invokeSource(src) {
|
|
|
963
1348
|
}
|
|
964
1349
|
/**
|
|
965
1350
|
* Reduces a machine config to the structural facts the emitted introspection
|
|
966
|
-
* test pins (
|
|
1351
|
+
* test pins (verification-4): captain bindings with result keys and every transition
|
|
967
1352
|
* arm, the quiescent states' event surfaces, the root event surface, and the
|
|
968
1353
|
* `BOSS_INTERRUPT` jumpable set.
|
|
969
1354
|
*/
|
|
@@ -990,6 +1375,7 @@ export function pinIntrospection(config) {
|
|
|
990
1375
|
: {}),
|
|
991
1376
|
sourceItem: binding.state.sourceItem,
|
|
992
1377
|
player: binding.state.player,
|
|
1378
|
+
...(binding.state.role !== undefined ? { role: binding.state.role } : {}),
|
|
993
1379
|
resultKeys: Object.keys(binding.state.result).sort(),
|
|
994
1380
|
onDone: normalizeArms(binding.invoke.onDone),
|
|
995
1381
|
onError: normalizeArms(binding.invoke.onError),
|
|
@@ -1061,7 +1447,7 @@ export function pinIntrospection(config) {
|
|
|
1061
1447
|
};
|
|
1062
1448
|
}
|
|
1063
1449
|
/*
|
|
1064
|
-
* Prompt-contract capture and composition checks (
|
|
1450
|
+
* Prompt-contract capture and composition checks (verification-5).
|
|
1065
1451
|
*
|
|
1066
1452
|
* The contract is derived from the artifacts, never hand-authored: context
|
|
1067
1453
|
* reads are traced through each state's `invoke.input` thunk with a recording
|
|
@@ -1079,6 +1465,7 @@ export const BOSS_REPLY_LABEL = 'Boss reply:';
|
|
|
1079
1465
|
// labelled form as well as natural-language variants; occurrence deltas below
|
|
1080
1466
|
// keep self-hosting prompt bodies free to quote either marker verbatim.
|
|
1081
1467
|
const PLAYER_BINDING_MARKER = /\bplayer\s+binding\b|(?:^|\n)[ \t]*player[ \t]*:[ \t]*(?=\S)/gi;
|
|
1468
|
+
const ROLE_BINDING_MARKER = /\brole\s+binding\b|(?:^|\n)[ \t]*role[ \t]*:[ \t]*(?=\S)/gi;
|
|
1082
1469
|
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
1470
|
const PLACEHOLDER = /<[^\s<>`]{1,60}>/g;
|
|
1084
1471
|
/** Lists the distinct `<...>` placeholder tokens in a prompt body, in order. */
|
|
@@ -1139,7 +1526,7 @@ function carriesSentinel(value, sentinel) {
|
|
|
1139
1526
|
}
|
|
1140
1527
|
/**
|
|
1141
1528
|
* Derives every captain state's prompt contract from the machine config
|
|
1142
|
-
* (
|
|
1529
|
+
* (verification-5): traced context reads, sentinel-traced input wiring, and the
|
|
1143
1530
|
* prompt body's placeholder tokens.
|
|
1144
1531
|
*/
|
|
1145
1532
|
export function capturePromptContract(config) {
|
|
@@ -1167,6 +1554,7 @@ export function capturePromptContract(config) {
|
|
|
1167
1554
|
state: state.stateId,
|
|
1168
1555
|
sourceItem: state.sourceItem,
|
|
1169
1556
|
player: state.player,
|
|
1557
|
+
...(state.role !== undefined ? { role: state.role } : {}),
|
|
1170
1558
|
reads,
|
|
1171
1559
|
wires,
|
|
1172
1560
|
placeholders: placeholdersIn(state.prompt),
|
|
@@ -1178,7 +1566,7 @@ export function capturePromptContract(config) {
|
|
|
1178
1566
|
* Derives, per captain state, which of its prompt's placeholder tokens the
|
|
1179
1567
|
* linked composer substitutes when the wired context is present — pinned into
|
|
1180
1568
|
* the emitted test so a token that later leaks unsubstituted fails it
|
|
1181
|
-
* (
|
|
1569
|
+
* (verification-5).
|
|
1182
1570
|
*/
|
|
1183
1571
|
export function deriveSubstitutions(config, compose, actor) {
|
|
1184
1572
|
const out = {};
|
|
@@ -1190,7 +1578,7 @@ export function deriveSubstitutions(config, compose, actor) {
|
|
|
1190
1578
|
continue;
|
|
1191
1579
|
try {
|
|
1192
1580
|
const reads = probeContextReads(inputFn);
|
|
1193
|
-
const composed = compose
|
|
1581
|
+
const composed = composeForState(compose, state, inputFn({ context: ordinaryContext(reads) }));
|
|
1194
1582
|
if (typeof composed !== 'string') {
|
|
1195
1583
|
out[state.stateId] = [];
|
|
1196
1584
|
continue;
|
|
@@ -1201,8 +1589,9 @@ export function deriveSubstitutions(config, compose, actor) {
|
|
|
1201
1589
|
// hide valid evidence, while merely deleting a token still cannot
|
|
1202
1590
|
// masquerade as substitution.
|
|
1203
1591
|
const evidenced = new Set();
|
|
1592
|
+
const promptReads = promptSentinelFields(state, reads);
|
|
1204
1593
|
for (const line of state.prompt.split('\n')) {
|
|
1205
|
-
for (const token of matchPromptBody(line, composed,
|
|
1594
|
+
for (const token of matchPromptBody(line, composed, promptReads)
|
|
1206
1595
|
?.substitutions ?? []) {
|
|
1207
1596
|
evidenced.add(token);
|
|
1208
1597
|
}
|
|
@@ -1217,7 +1606,7 @@ export function deriveSubstitutions(config, compose, actor) {
|
|
|
1217
1606
|
}
|
|
1218
1607
|
/**
|
|
1219
1608
|
* Checks the linked composer against the link contract for every captain state
|
|
1220
|
-
* (
|
|
1609
|
+
* (verification-5), returning findings (empty when conformant): the prompt body is
|
|
1221
1610
|
* preserved modulo substituted placeholders, the adjudicator-facing Boss-reply
|
|
1222
1611
|
* contract never leaks into a player prompt, no continuation appears on an
|
|
1223
1612
|
* ordinary turn, and a Boss-reply continuation turn opens with the exact
|
|
@@ -1225,19 +1614,30 @@ export function deriveSubstitutions(config, compose, actor) {
|
|
|
1225
1614
|
*/
|
|
1226
1615
|
export function checkPromptComposition(opts) {
|
|
1227
1616
|
const findings = [];
|
|
1617
|
+
const controller = isControllerMachine(opts.config);
|
|
1618
|
+
const schemaResolution = resolveArtifactSchemaForVerification({
|
|
1619
|
+
config: opts.config,
|
|
1620
|
+
...(opts.artifactSchema === undefined
|
|
1621
|
+
? {}
|
|
1622
|
+
: { artifactSchema: opts.artifactSchema }),
|
|
1623
|
+
});
|
|
1624
|
+
findings.push(...schemaResolution.findings);
|
|
1625
|
+
const inferredArtifactSchema = schemaResolution.artifactSchema;
|
|
1228
1626
|
const substitutions = deriveSubstitutions(opts.config, opts.compose, opts.actor);
|
|
1229
1627
|
const composerName = opts.actor === 'captain' ? 'composeCaptainPrompt' : 'composePlayerPrompt';
|
|
1230
|
-
|
|
1628
|
+
const bindings = enumerateCaptainBindings(opts.config);
|
|
1629
|
+
for (const binding of bindings) {
|
|
1231
1630
|
const { state, inputFn } = binding;
|
|
1232
1631
|
if (opts.actor !== undefined && state.actor !== opts.actor)
|
|
1233
1632
|
continue;
|
|
1234
1633
|
if (typeof inputFn !== 'function')
|
|
1235
1634
|
continue;
|
|
1236
1635
|
const reads = probeContextReads(inputFn);
|
|
1636
|
+
const promptReads = promptSentinelFields(state, reads);
|
|
1237
1637
|
const substituted = substitutions[state.stateId] ?? [];
|
|
1238
1638
|
let ordinary;
|
|
1239
1639
|
try {
|
|
1240
|
-
ordinary = opts.compose
|
|
1640
|
+
ordinary = composeForState(opts.compose, state, inputFn({ context: ordinaryContext(reads) }));
|
|
1241
1641
|
if (typeof ordinary !== 'string') {
|
|
1242
1642
|
throw new Error(`${composerName} returned a non-string value`);
|
|
1243
1643
|
}
|
|
@@ -1246,8 +1646,8 @@ export function checkPromptComposition(opts) {
|
|
|
1246
1646
|
findings.push(`${state.stateId}: ${composerName} threw on an ordinary turn: ${messageOf(error)}`);
|
|
1247
1647
|
continue;
|
|
1248
1648
|
}
|
|
1249
|
-
findings.push(...bodyFindings(state, ordinary, substituted,
|
|
1250
|
-
pushUnique(findings, ...
|
|
1649
|
+
findings.push(...bodyFindings(state, ordinary, substituted, promptReads, 'ordinary'));
|
|
1650
|
+
pushUnique(findings, ...promptControlFindings(state, ordinary));
|
|
1251
1651
|
// A self-hosted playbook's domain body may legitimately quote the
|
|
1252
1652
|
// adjudicator contract or the continuation texts (it instructs a compiler
|
|
1253
1653
|
// about them); only occurrences the composer ADDS beyond the body's own
|
|
@@ -1259,15 +1659,34 @@ export function checkPromptComposition(opts) {
|
|
|
1259
1659
|
if ([CONTINUATION_PREAMBLE, BOSS_QUESTION_LABEL, BOSS_REPLY_LABEL].some((needle) => occurrences(ordinary, needle) > occurrences(state.prompt, needle))) {
|
|
1260
1660
|
findings.push(`${state.stateId}: continuation blocks appear on an ordinary turn`);
|
|
1261
1661
|
}
|
|
1662
|
+
// Controllers own no Boss-reply wait. A missing ordinary result is already
|
|
1663
|
+
// diagnosed by conformance, so do not fabricate a continuation contract.
|
|
1664
|
+
if (controller || !Object.hasOwn(state.result, NEEDS_BOSS_REPLY))
|
|
1665
|
+
continue;
|
|
1666
|
+
const artifactSchema = schemaResolution.findings.length > 0
|
|
1667
|
+
? undefined
|
|
1668
|
+
: (inferredArtifactSchema ??
|
|
1669
|
+
(state.role !== undefined ? 3 : state.player !== '' ? 1 : undefined));
|
|
1670
|
+
if (artifactSchema === undefined) {
|
|
1671
|
+
findings.push(`${state.stateId}: prompt composition requires artifactSchema 1 or 3 to probe this direct-Captain continuation`);
|
|
1672
|
+
continue;
|
|
1673
|
+
}
|
|
1262
1674
|
// A Boss-reply continuation turn: the thunk carries the pending question
|
|
1263
1675
|
// and reply, and the composer opens with the exact preamble and labelled
|
|
1264
1676
|
// Q&A blocks before the domain body (gears2fsm.md, link.md).
|
|
1265
1677
|
const question = sentinelFor('question');
|
|
1266
1678
|
const reply = sentinelFor('bossReply');
|
|
1267
1679
|
const pendingBossQuestion = {
|
|
1680
|
+
...(artifactSchema === 3
|
|
1681
|
+
? state.actor === 'captain'
|
|
1682
|
+
? { asker: { kind: 'captain' } }
|
|
1683
|
+
: { asker: { kind: 'role', roleId: state.role ?? '' } }
|
|
1684
|
+
: {
|
|
1685
|
+
player: state.actor === 'captain' ? 'Captain' : state.player,
|
|
1686
|
+
}),
|
|
1687
|
+
questionId: state.stateId,
|
|
1268
1688
|
resumeStateId: state.stateId,
|
|
1269
1689
|
sourceItem: state.sourceItem,
|
|
1270
|
-
player: state.player,
|
|
1271
1690
|
question,
|
|
1272
1691
|
};
|
|
1273
1692
|
let continuation;
|
|
@@ -1284,7 +1703,7 @@ export function checkPromptComposition(opts) {
|
|
|
1284
1703
|
bossReplies: { [state.stateId]: reply },
|
|
1285
1704
|
},
|
|
1286
1705
|
});
|
|
1287
|
-
continuation = opts.compose
|
|
1706
|
+
continuation = composeForState(opts.compose, state, input);
|
|
1288
1707
|
if (typeof continuation !== 'string') {
|
|
1289
1708
|
throw new Error(`${composerName} returned a non-string value`);
|
|
1290
1709
|
}
|
|
@@ -1300,7 +1719,7 @@ export function checkPromptComposition(opts) {
|
|
|
1300
1719
|
if (!continuation.startsWith(`${CONTINUATION_PREAMBLE}\n\n`)) {
|
|
1301
1720
|
findings.push(`${state.stateId}: a continuation turn does not open with the exact preamble`);
|
|
1302
1721
|
}
|
|
1303
|
-
const bodyStart = bodyIndex(state, continuation, substituted,
|
|
1722
|
+
const bodyStart = bodyIndex(state, continuation, substituted, promptReads);
|
|
1304
1723
|
const questionBlock = `${BOSS_QUESTION_LABEL}\n${question}`;
|
|
1305
1724
|
const replyBlock = `${BOSS_REPLY_LABEL}\n${reply}`;
|
|
1306
1725
|
for (const [label, value] of [
|
|
@@ -1322,21 +1741,59 @@ export function checkPromptComposition(opts) {
|
|
|
1322
1741
|
if (!continuation.startsWith(exactContinuationPrefix)) {
|
|
1323
1742
|
findings.push(`${state.stateId}: a continuation turn does not preserve the exact ordered Boss question/reply blocks`);
|
|
1324
1743
|
}
|
|
1325
|
-
findings.push(...bodyFindings(state, continuation, substituted,
|
|
1326
|
-
pushUnique(findings, ...
|
|
1744
|
+
findings.push(...bodyFindings(state, continuation, substituted, promptReads, 'continuation'));
|
|
1745
|
+
pushUnique(findings, ...promptControlFindings(state, continuation));
|
|
1327
1746
|
}
|
|
1328
1747
|
return findings;
|
|
1329
1748
|
}
|
|
1330
|
-
function
|
|
1331
|
-
|
|
1332
|
-
|
|
1749
|
+
function promptSentinelFields(state, reads) {
|
|
1750
|
+
return state.role === undefined
|
|
1751
|
+
? [...reads]
|
|
1752
|
+
: [...reads, `promptIdentity:${state.role}`];
|
|
1753
|
+
}
|
|
1754
|
+
function composeForState(compose, state, input) {
|
|
1755
|
+
// Schema-1 composers and the shared default composer use their second
|
|
1756
|
+
// positional argument as a placeholder-field map. A callable proxy with a
|
|
1757
|
+
// property-clean view is therefore both an invocation-scoped schema-3 lookup
|
|
1758
|
+
// and an empty map to historical/default composition, without Function.name,
|
|
1759
|
+
// Function.length, or Function.prototype token collisions.
|
|
1760
|
+
const lookup = (roleId) => {
|
|
1761
|
+
if (state.role === undefined) {
|
|
1762
|
+
throw new Error(`prompt identity lookup used role ${JSON.stringify(roleId)} for a direct-Captain or historical state`);
|
|
1763
|
+
}
|
|
1764
|
+
if (roleId !== state.role) {
|
|
1765
|
+
throw new Error(`prompt identity lookup used role ${JSON.stringify(roleId)} instead of canonical local role ${JSON.stringify(state.role)}`);
|
|
1766
|
+
}
|
|
1767
|
+
return sentinelFor(`promptIdentity:${roleId}`);
|
|
1768
|
+
};
|
|
1769
|
+
const promptIdentity = new Proxy(lookup, {
|
|
1770
|
+
get: () => undefined,
|
|
1771
|
+
has: () => false,
|
|
1772
|
+
ownKeys: () => [],
|
|
1773
|
+
getOwnPropertyDescriptor: () => undefined,
|
|
1774
|
+
});
|
|
1775
|
+
return state.actor === 'player' && state.role !== undefined
|
|
1776
|
+
? compose(input, promptIdentity)
|
|
1777
|
+
: compose(input);
|
|
1778
|
+
}
|
|
1779
|
+
function promptControlFindings(state, composed) {
|
|
1333
1780
|
const findings = [];
|
|
1334
|
-
|
|
1335
|
-
patternOccurrences(state.prompt, PLAYER_BINDING_MARKER)
|
|
1781
|
+
const introducesPlayerBinding = patternOccurrences(composed, PLAYER_BINDING_MARKER) >
|
|
1782
|
+
patternOccurrences(state.prompt, PLAYER_BINDING_MARKER);
|
|
1783
|
+
if (state.actor === 'captain' && introducesPlayerBinding) {
|
|
1336
1784
|
findings.push(`${state.stateId}: composeCaptainPrompt introduces a player binding into a direct-Captain prompt`);
|
|
1337
1785
|
}
|
|
1338
|
-
if (
|
|
1339
|
-
|
|
1786
|
+
else if (state.role !== undefined && introducesPlayerBinding) {
|
|
1787
|
+
findings.push(`${state.stateId}: composePlayerPrompt exposes a concrete player binding in a schema-3 delegated-role prompt`);
|
|
1788
|
+
}
|
|
1789
|
+
if (state.actor === 'captain' &&
|
|
1790
|
+
patternOccurrences(composed, ROLE_BINDING_MARKER) >
|
|
1791
|
+
patternOccurrences(state.prompt, ROLE_BINDING_MARKER)) {
|
|
1792
|
+
findings.push(`${state.stateId}: composeCaptainPrompt introduces a role binding into a direct-Captain prompt`);
|
|
1793
|
+
}
|
|
1794
|
+
if (state.actor === 'captain' &&
|
|
1795
|
+
patternOccurrences(composed, PLAYER_RESUME_MARKER) >
|
|
1796
|
+
patternOccurrences(state.prompt, PLAYER_RESUME_MARKER)) {
|
|
1340
1797
|
findings.push(`${state.stateId}: composeCaptainPrompt introduces a player resume instruction into a direct-Captain prompt`);
|
|
1341
1798
|
}
|
|
1342
1799
|
return findings;
|
|
@@ -1429,7 +1886,7 @@ function matchPromptBody(prompt, composed, reads, expectedSubstitutions) {
|
|
|
1429
1886
|
}
|
|
1430
1887
|
return null;
|
|
1431
1888
|
}
|
|
1432
|
-
/** Findings when a composed prompt does not preserve the domain body (
|
|
1889
|
+
/** Findings when a composed prompt does not preserve the domain body (verification-5). */
|
|
1433
1890
|
function bodyFindings(state, composed, substituted, reads, turn) {
|
|
1434
1891
|
if (matchPromptBody(state.prompt, composed, reads, substituted) !== null) {
|
|
1435
1892
|
return [];
|
|
@@ -1454,7 +1911,7 @@ function bodyFindings(state, composed, substituted, reads, turn) {
|
|
|
1454
1911
|
function bodyIndex(state, composed, substituted, reads) {
|
|
1455
1912
|
return (matchPromptBody(state.prompt, composed, reads, substituted)?.index ?? -1);
|
|
1456
1913
|
}
|
|
1457
|
-
// Local copy: this module is copied verbatim beside the artifact (
|
|
1914
|
+
// Local copy: this module is copied verbatim beside the artifact (verification-12),
|
|
1458
1915
|
// so it may not import a sibling module.
|
|
1459
1916
|
function messageOf(error) {
|
|
1460
1917
|
return error instanceof Error ? error.message : String(error);
|
|
@@ -1492,6 +1949,12 @@ export function findMachineConfig(fsmModule) {
|
|
|
1492
1949
|
}
|
|
1493
1950
|
throw new Error('fsm module exports no XState machine with a `.config.states`');
|
|
1494
1951
|
}
|
|
1952
|
+
/** Reads the schema-3 cohort declaration from an imported FSM module. */
|
|
1953
|
+
export function findConcurrentRoleSets(fsmModule) {
|
|
1954
|
+
if (typeof fsmModule !== 'object' || fsmModule === null)
|
|
1955
|
+
return undefined;
|
|
1956
|
+
return fsmModule.concurrentRoleSets;
|
|
1957
|
+
}
|
|
1495
1958
|
/**
|
|
1496
1959
|
* Builds a per-artifact vitest module that fails when the compiled FSM drifts
|
|
1497
1960
|
* from its GEARS source: it reads the artifact's `gears` file and the machine its
|
|
@@ -1502,22 +1965,35 @@ export function generateGearsFsmConformanceTest(opts) {
|
|
|
1502
1965
|
return `// SPDX-License-Identifier: Apache-2.0
|
|
1503
1966
|
// SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
|
|
1504
1967
|
|
|
1505
|
-
// Generated by slc (
|
|
1968
|
+
// Generated by slc (DR-009): GEARS↔FSM conformance.
|
|
1506
1969
|
import { readFileSync } from 'node:fs';
|
|
1507
1970
|
import { fileURLToPath } from 'node:url';
|
|
1508
1971
|
|
|
1509
1972
|
import { describe, expect, it } from 'vitest';
|
|
1510
1973
|
|
|
1511
|
-
import { checkGearsFsmConformance, findMachineConfig } from ${sourceString(opts.verifyModule)};
|
|
1974
|
+
import { checkGearsFsmConformance, findConcurrentRoleSets, findMachineConfig } from ${sourceString(opts.verifyModule)};
|
|
1512
1975
|
import * as fsm from ${sourceString(opts.fsmModule)};
|
|
1513
1976
|
|
|
1977
|
+
const SCHEMA_FINDINGS = ${JSON.stringify(opts.schemaFindings ?? [], null, 2)};
|
|
1978
|
+
|
|
1514
1979
|
describe(${sourceString(`${opts.basename}: GEARS↔FSM conformance`)}, () => {
|
|
1980
|
+
it('uses consistent artifact-schema evidence', () => {
|
|
1981
|
+
expect(SCHEMA_FINDINGS).toEqual([]);
|
|
1982
|
+
});
|
|
1983
|
+
|
|
1515
1984
|
it('maps every GEARS item to a state with its player and verbatim prompt', () => {
|
|
1516
1985
|
const gears = readFileSync(
|
|
1517
1986
|
fileURLToPath(new URL(${sourceString(opts.gearsFile)}, import.meta.url)),
|
|
1518
1987
|
'utf8',
|
|
1519
1988
|
);
|
|
1520
|
-
expect(
|
|
1989
|
+
expect(
|
|
1990
|
+
checkGearsFsmConformance(gears, findMachineConfig(fsm), {
|
|
1991
|
+
concurrentRoleSets: findConcurrentRoleSets(fsm),
|
|
1992
|
+
${opts.artifactSchema === undefined
|
|
1993
|
+
? ''
|
|
1994
|
+
: `artifactSchema: ${opts.artifactSchema},`}
|
|
1995
|
+
}),
|
|
1996
|
+
).toEqual([]);
|
|
1521
1997
|
});
|
|
1522
1998
|
});
|
|
1523
1999
|
`;
|
|
@@ -1526,7 +2002,7 @@ describe(${sourceString(`${opts.basename}: GEARS↔FSM conformance`)}, () => {
|
|
|
1526
2002
|
* Emits the GEARS↔FSM conformance test as `slc` output beside a compiled
|
|
1527
2003
|
* `playbook` artifact: writes `<basename>.gears-fsm.test.ts` into the artifact
|
|
1528
2004
|
* directory (`<basename>.playbook/`), wiring the artifact's `gears` file and its
|
|
1529
|
-
* `fsm` module's machine to the checker, and returns the written path (
|
|
2005
|
+
* `fsm` module's machine to the checker, and returns the written path (verification-2;
|
|
1530
2006
|
* [DR-009](../decisions/009-slc-playbook-pipeline-compilation.md)).
|
|
1531
2007
|
*/
|
|
1532
2008
|
export async function emitGearsFsmConformanceTest(opts) {
|
|
@@ -1537,6 +2013,10 @@ export async function emitGearsFsmConformanceTest(opts) {
|
|
|
1537
2013
|
fsmModule: `./${opts.basename}.fsm.js`,
|
|
1538
2014
|
gearsFile: `./${opts.basename}.gears.md`,
|
|
1539
2015
|
verifyModule: opts.verifyModule ?? VERIFY_MODULE,
|
|
2016
|
+
...(opts.artifactSchema === undefined
|
|
2017
|
+
? {}
|
|
2018
|
+
: { artifactSchema: opts.artifactSchema }),
|
|
2019
|
+
schemaFindings: opts.schemaFindings,
|
|
1540
2020
|
});
|
|
1541
2021
|
await mkdir(opts.artifactDir, { recursive: true });
|
|
1542
2022
|
const path = join(opts.artifactDir, `${opts.basename}.gears-fsm.test.ts`);
|
|
@@ -1558,14 +2038,15 @@ export async function loadFsmModule(fsmPath) {
|
|
|
1558
2038
|
return import(url.href);
|
|
1559
2039
|
}
|
|
1560
2040
|
/**
|
|
1561
|
-
* Imports
|
|
1562
|
-
*
|
|
1563
|
-
* `./<basename>.fsm.js` edge,
|
|
1564
|
-
* `./<basename>.fsm.ts` exists. Stage a
|
|
1565
|
-
* module specifier points at the
|
|
1566
|
-
*
|
|
2041
|
+
* Imports generated linked TypeScript for emission-time, standalone, or
|
|
2042
|
+
* equivalence review before its sibling FSM has been built to JavaScript.
|
|
2043
|
+
* NodeNext source correctly names the runtime-safe `./<basename>.fsm.js` edge,
|
|
2044
|
+
* but review may run while only `./<basename>.fsm.ts` exists. Stage a
|
|
2045
|
+
* same-directory copy whose one generated module specifier points at the
|
|
2046
|
+
* hashed TypeScript artifact, import that copy, and remove it without changing
|
|
2047
|
+
* the linked source or its production import.
|
|
1567
2048
|
*/
|
|
1568
|
-
async function loadLinkedModuleForVerification(opts) {
|
|
2049
|
+
export async function loadLinkedModuleForVerification(opts) {
|
|
1569
2050
|
const linkedSource = await readFile(opts.linkedPath, 'utf8');
|
|
1570
2051
|
const fsmStem = basename(opts.fsmPath, '.ts');
|
|
1571
2052
|
const runtimeSpecifier = `./${fsmStem}.js`;
|
|
@@ -1590,7 +2071,7 @@ async function loadLinkedModuleForVerification(opts) {
|
|
|
1590
2071
|
}
|
|
1591
2072
|
/**
|
|
1592
2073
|
* Builds a per-artifact vitest module that fails when the machine's structure
|
|
1593
|
-
* drifts from the topology pinned at build time (
|
|
2074
|
+
* drifts from the topology pinned at build time (verification-4).
|
|
1594
2075
|
*/
|
|
1595
2076
|
export function generateFsmIntrospectionTest(opts) {
|
|
1596
2077
|
return `// SPDX-License-Identifier: Apache-2.0
|
|
@@ -1615,7 +2096,7 @@ describe(${sourceString(`${opts.basename}: FSM introspection`)}, () => {
|
|
|
1615
2096
|
}
|
|
1616
2097
|
/**
|
|
1617
2098
|
* Builds a per-artifact vitest module pinning the prompt contract derived from
|
|
1618
|
-
* the artifacts at build time (
|
|
2099
|
+
* the artifacts at build time (verification-5): the per-state context reads, input
|
|
1619
2100
|
* wiring, and placeholders always; and, when the linked module exposes its
|
|
1620
2101
|
* matching Captain/player composers, the composition checks and pinned
|
|
1621
2102
|
* substitution maps.
|
|
@@ -1650,6 +2131,9 @@ const ${compose} = (
|
|
|
1650
2131
|
config: findMachineConfig(fsm),
|
|
1651
2132
|
compose: ${compose},
|
|
1652
2133
|
actor: '${actor}',
|
|
2134
|
+
${opts.artifactSchema === undefined
|
|
2135
|
+
? ''
|
|
2136
|
+
: `artifactSchema: ${opts.artifactSchema},`}
|
|
1653
2137
|
}),
|
|
1654
2138
|
).toEqual([]);
|
|
1655
2139
|
});
|
|
@@ -1684,17 +2168,157 @@ import {
|
|
|
1684
2168
|
import * as fsm from ${sourceString(opts.fsmModule)};
|
|
1685
2169
|
${composerImports}
|
|
1686
2170
|
const CONTRACT = ${JSON.stringify(opts.rows, null, 2)};
|
|
2171
|
+
const SCHEMA_FINDINGS = ${JSON.stringify(opts.schemaFindings ?? [], null, 2)};
|
|
1687
2172
|
|
|
1688
2173
|
describe(${sourceString(`${opts.basename}: prompt contract`)}, () => {
|
|
2174
|
+
it('uses consistent artifact-schema evidence', () => {
|
|
2175
|
+
expect(SCHEMA_FINDINGS).toEqual([]);
|
|
2176
|
+
});
|
|
2177
|
+
|
|
1689
2178
|
it('matches the prompt contract pinned at build time', () => {
|
|
1690
2179
|
expect(capturePromptContract(findMachineConfig(fsm))).toEqual(CONTRACT);
|
|
1691
2180
|
});
|
|
1692
2181
|
${composerBlock}});
|
|
1693
2182
|
`;
|
|
1694
2183
|
}
|
|
2184
|
+
function promptArtifactSchemaSignalsFromConfig(config) {
|
|
2185
|
+
const states = enumerateCaptainStates(config);
|
|
2186
|
+
return {
|
|
2187
|
+
schema1: states.some(({ player }) => player !== ''),
|
|
2188
|
+
schema3: isControllerMachine(config) ||
|
|
2189
|
+
states.some(({ role }) => role !== undefined),
|
|
2190
|
+
};
|
|
2191
|
+
}
|
|
2192
|
+
function linkedArtifactSchemaSignal(linked) {
|
|
2193
|
+
const factory = linked.default;
|
|
2194
|
+
if (typeof factory !== 'function') {
|
|
2195
|
+
return { historicalFallback: false, invalidCompatibility: false };
|
|
2196
|
+
}
|
|
2197
|
+
if (!Object.hasOwn(factory, 'compat')) {
|
|
2198
|
+
return { historicalFallback: true, invalidCompatibility: false };
|
|
2199
|
+
}
|
|
2200
|
+
const descriptor = Object.getOwnPropertyDescriptor(factory, 'compat');
|
|
2201
|
+
if (descriptor === undefined ||
|
|
2202
|
+
!Object.hasOwn(descriptor, 'value') ||
|
|
2203
|
+
descriptor.enumerable !== true ||
|
|
2204
|
+
descriptor.writable !== false ||
|
|
2205
|
+
descriptor.configurable !== false) {
|
|
2206
|
+
return { historicalFallback: false, invalidCompatibility: true };
|
|
2207
|
+
}
|
|
2208
|
+
const compat = descriptor.value;
|
|
2209
|
+
if (typeof compat !== 'object' ||
|
|
2210
|
+
compat === null ||
|
|
2211
|
+
Array.isArray(compat) ||
|
|
2212
|
+
Object.getPrototypeOf(compat) !== Object.prototype ||
|
|
2213
|
+
!Object.isFrozen(compat) ||
|
|
2214
|
+
Object.getOwnPropertySymbols(compat).length !== 0) {
|
|
2215
|
+
return { historicalFallback: false, invalidCompatibility: true };
|
|
2216
|
+
}
|
|
2217
|
+
const names = Object.getOwnPropertyNames(compat);
|
|
2218
|
+
const artifactSchema = Object.getOwnPropertyDescriptor(compat, 'artifactSchema');
|
|
2219
|
+
const runtimeAbi = Object.getOwnPropertyDescriptor(compat, 'runtimeAbi');
|
|
2220
|
+
const exact = names.length === 2 &&
|
|
2221
|
+
names.includes('artifactSchema') &&
|
|
2222
|
+
names.includes('runtimeAbi') &&
|
|
2223
|
+
artifactSchema?.enumerable === true &&
|
|
2224
|
+
Object.hasOwn(artifactSchema, 'value') &&
|
|
2225
|
+
artifactSchema.value === 3 &&
|
|
2226
|
+
runtimeAbi?.enumerable === true &&
|
|
2227
|
+
Object.hasOwn(runtimeAbi, 'value') &&
|
|
2228
|
+
runtimeAbi.value === 1;
|
|
2229
|
+
return exact
|
|
2230
|
+
? { schema: 3, historicalFallback: false, invalidCompatibility: false }
|
|
2231
|
+
: { historicalFallback: false, invalidCompatibility: true };
|
|
2232
|
+
}
|
|
2233
|
+
/**
|
|
2234
|
+
* Schema decision shared by generated and standalone artifact verification
|
|
2235
|
+
* (verification-21). Reviewed provenance is evidence through its exact
|
|
2236
|
+
* historical map; any other provenance is evidence only through the engine
|
|
2237
|
+
* declaration read from the link target's installed package — `RUNTIME_ABI`
|
|
2238
|
+
* `1` with artifact schema `3` — and is otherwise reported unsupported
|
|
2239
|
+
* (DR-028).
|
|
2240
|
+
*/
|
|
2241
|
+
export function resolveArtifactSchemaForVerification(opts) {
|
|
2242
|
+
const candidates = [];
|
|
2243
|
+
const invalidSignalFindings = [];
|
|
2244
|
+
if (opts.artifactSchema !== undefined) {
|
|
2245
|
+
candidates.push({
|
|
2246
|
+
source: 'review-supplied artifact schema',
|
|
2247
|
+
schema: opts.artifactSchema,
|
|
2248
|
+
});
|
|
2249
|
+
}
|
|
2250
|
+
const provenance = opts.provenance ?? opts.runtimeDeclaration?.provenance;
|
|
2251
|
+
const provenanceSchema = artifactSchemaForPlaybookProvenance(provenance);
|
|
2252
|
+
if (provenanceSchema !== undefined) {
|
|
2253
|
+
candidates.push({
|
|
2254
|
+
source: 'reviewed link-target provenance',
|
|
2255
|
+
schema: provenanceSchema,
|
|
2256
|
+
});
|
|
2257
|
+
}
|
|
2258
|
+
else if (provenance !== undefined) {
|
|
2259
|
+
if (opts.runtimeDeclaration === undefined) {
|
|
2260
|
+
invalidSignalFindings.push(`artifact schema has unsupported link-target provenance ${JSON.stringify(provenance)}`);
|
|
2261
|
+
}
|
|
2262
|
+
else if (declaresComposedV3(opts.runtimeDeclaration)) {
|
|
2263
|
+
candidates.push({ source: 'declared link-target contract', schema: 3 });
|
|
2264
|
+
}
|
|
2265
|
+
else {
|
|
2266
|
+
invalidSignalFindings.push(`artifact schema has an unsupported link-target contract: ${describeRuntimeDeclaration(opts.runtimeDeclaration)}`);
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
if (opts.config !== undefined) {
|
|
2270
|
+
const configSignals = promptArtifactSchemaSignalsFromConfig(opts.config);
|
|
2271
|
+
if (configSignals.schema1)
|
|
2272
|
+
candidates.push({ source: 'FSM historical-player structure', schema: 1 });
|
|
2273
|
+
if (configSignals.schema3)
|
|
2274
|
+
candidates.push({ source: 'FSM role/controller structure', schema: 3 });
|
|
2275
|
+
}
|
|
2276
|
+
const linkedSignal = opts.linked === undefined
|
|
2277
|
+
? undefined
|
|
2278
|
+
: linkedArtifactSchemaSignal(opts.linked);
|
|
2279
|
+
if (linkedSignal?.schema !== undefined) {
|
|
2280
|
+
candidates.push({
|
|
2281
|
+
source: 'linked factory compatibility',
|
|
2282
|
+
schema: linkedSignal.schema,
|
|
2283
|
+
});
|
|
2284
|
+
}
|
|
2285
|
+
if (linkedSignal?.invalidCompatibility) {
|
|
2286
|
+
invalidSignalFindings.push('linked factory has an own compatibility declaration that is not exact immutable schema 3/runtime ABI 1');
|
|
2287
|
+
}
|
|
2288
|
+
if (invalidSignalFindings.length > 0) {
|
|
2289
|
+
return { findings: invalidSignalFindings };
|
|
2290
|
+
}
|
|
2291
|
+
const schemas = new Set(candidates.map(({ schema }) => schema));
|
|
2292
|
+
if (schemas.size > 1) {
|
|
2293
|
+
return {
|
|
2294
|
+
findings: [
|
|
2295
|
+
`artifact schema signals disagree (${candidates
|
|
2296
|
+
.map(({ source, schema }) => `${source}: ${schema}`)
|
|
2297
|
+
.join(', ')})`,
|
|
2298
|
+
],
|
|
2299
|
+
};
|
|
2300
|
+
}
|
|
2301
|
+
const [artifactSchema] = schemas;
|
|
2302
|
+
if (artifactSchema !== undefined) {
|
|
2303
|
+
return { artifactSchema, findings: [] };
|
|
2304
|
+
}
|
|
2305
|
+
if (linkedSignal?.historicalFallback) {
|
|
2306
|
+
return { artifactSchema: 1, findings: [] };
|
|
2307
|
+
}
|
|
2308
|
+
const hasAmbiguousCaptainContinuation = opts.config !== undefined &&
|
|
2309
|
+
enumerateCaptainStates(opts.config).some((state) => state.actor === 'captain' &&
|
|
2310
|
+
Object.hasOwn(state.result, NEEDS_BOSS_REPLY));
|
|
2311
|
+
return hasAmbiguousCaptainContinuation
|
|
2312
|
+
? {
|
|
2313
|
+
findings: [
|
|
2314
|
+
'artifact schema has no reviewed provenance, generation-specific actor structure, or callable linked factory for a direct-Captain continuation',
|
|
2315
|
+
],
|
|
2316
|
+
}
|
|
2317
|
+
: { findings: [] };
|
|
2318
|
+
}
|
|
1695
2319
|
/**
|
|
1696
2320
|
* Emits the prompt-contract test beside a compiled `playbook` artifact
|
|
1697
|
-
* (
|
|
2321
|
+
* (verification-5): derives and pins the per-state contract from the physical
|
|
1698
2322
|
* `<basename>.fsm.ts` artifact, then emits NodeNext `.js` imports for that FSM
|
|
1699
2323
|
* and any linked `<basename>.playbook.ts` module. When the linked module
|
|
1700
2324
|
* exposes the `_internal` composer matching each state actor —
|
|
@@ -1711,6 +2335,20 @@ export async function emitPromptContractTest(opts) {
|
|
|
1711
2335
|
const fsmPath = join(opts.artifactDir, `${opts.basename}.fsm.ts`);
|
|
1712
2336
|
const config = findMachineConfig(await loadFsmModule(fsmPath));
|
|
1713
2337
|
const rows = capturePromptContract(config);
|
|
2338
|
+
const evidence = {
|
|
2339
|
+
...(opts.provenance === undefined ? {} : { provenance: opts.provenance }),
|
|
2340
|
+
...(opts.runtimeDeclaration === undefined
|
|
2341
|
+
? {}
|
|
2342
|
+
: { runtimeDeclaration: opts.runtimeDeclaration }),
|
|
2343
|
+
...(opts.artifactSchema === undefined
|
|
2344
|
+
? {}
|
|
2345
|
+
: { artifactSchema: opts.artifactSchema }),
|
|
2346
|
+
};
|
|
2347
|
+
let schemaResolution = resolveArtifactSchemaForVerification({
|
|
2348
|
+
config,
|
|
2349
|
+
...evidence,
|
|
2350
|
+
});
|
|
2351
|
+
let artifactSchema = schemaResolution.artifactSchema;
|
|
1714
2352
|
let composer;
|
|
1715
2353
|
const linkedPath = join(opts.artifactDir, `${opts.basename}.playbook.ts`);
|
|
1716
2354
|
if (existsSync(linkedPath)) {
|
|
@@ -1719,6 +2357,12 @@ export async function emitPromptContractTest(opts) {
|
|
|
1719
2357
|
linkedPath,
|
|
1720
2358
|
fsmPath,
|
|
1721
2359
|
}));
|
|
2360
|
+
schemaResolution = resolveArtifactSchemaForVerification({
|
|
2361
|
+
config,
|
|
2362
|
+
linked,
|
|
2363
|
+
...evidence,
|
|
2364
|
+
});
|
|
2365
|
+
artifactSchema = schemaResolution.artifactSchema;
|
|
1722
2366
|
const actors = new Set(enumerateCaptainStates(config).map(({ actor }) => actor));
|
|
1723
2367
|
const substitutions = {};
|
|
1724
2368
|
for (const actor of ['captain', 'player']) {
|
|
@@ -1736,6 +2380,7 @@ export async function emitPromptContractTest(opts) {
|
|
|
1736
2380
|
config,
|
|
1737
2381
|
compose: typedCompose,
|
|
1738
2382
|
actor,
|
|
2383
|
+
...(artifactSchema === undefined ? {} : { artifactSchema }),
|
|
1739
2384
|
});
|
|
1740
2385
|
diagnostics.push(...findings.map((finding) => `prompt contract: ${finding}`));
|
|
1741
2386
|
}
|
|
@@ -1751,11 +2396,14 @@ export async function emitPromptContractTest(opts) {
|
|
|
1751
2396
|
diagnostics.push(`prompt contract: linked module could not be imported (${messageOf(error)}); composition checks not emitted`);
|
|
1752
2397
|
}
|
|
1753
2398
|
}
|
|
2399
|
+
diagnostics.unshift(...schemaResolution.findings.map((finding) => `prompt contract: ${finding}`));
|
|
1754
2400
|
const content = generatePromptContractTest({
|
|
1755
2401
|
basename: opts.basename,
|
|
1756
2402
|
fsmModule: `./${opts.basename}.fsm.js`,
|
|
1757
2403
|
verifyModule: opts.verifyModule ?? VERIFY_MODULE,
|
|
1758
2404
|
rows,
|
|
2405
|
+
...(artifactSchema === undefined ? {} : { artifactSchema }),
|
|
2406
|
+
schemaFindings: schemaResolution.findings,
|
|
1759
2407
|
composer,
|
|
1760
2408
|
});
|
|
1761
2409
|
await mkdir(opts.artifactDir, { recursive: true });
|
|
@@ -1765,7 +2413,7 @@ export async function emitPromptContractTest(opts) {
|
|
|
1765
2413
|
}
|
|
1766
2414
|
/**
|
|
1767
2415
|
* Emits the introspection test beside a compiled `playbook` artifact
|
|
1768
|
-
* (
|
|
2416
|
+
* (verification-4): derives topology pins from the physical `<basename>.fsm.ts`,
|
|
1769
2417
|
* emits a NodeNext `.js` import for that sibling source, and writes
|
|
1770
2418
|
* `<basename>.fsm.introspect.test.ts` into the artifact directory.
|
|
1771
2419
|
*
|
|
@@ -1785,7 +2433,7 @@ export async function emitFsmIntrospectionTest(opts) {
|
|
|
1785
2433
|
await writeFile(path, content);
|
|
1786
2434
|
return path;
|
|
1787
2435
|
}
|
|
1788
|
-
// Transition-coverage verification (
|
|
2436
|
+
// Transition-coverage verification (verification-6) lives in its own module — it
|
|
1789
2437
|
// depends on `xstate` to drive the machine — and is re-exported here so every
|
|
1790
2438
|
// generated test imports one checker module (`@sublang/slc/verify`).
|
|
1791
2439
|
export * from './verify-coverage.js';
|