@sublang/slc 0.7.0 → 0.9.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/dist/cligent-agent.d.ts.map +1 -1
- package/dist/cligent-agent.js +43 -3
- package/dist/cligent-agent.js.map +1 -1
- package/dist/compiled-executor.d.ts.map +1 -1
- package/dist/compiled-executor.js +63 -9
- package/dist/compiled-executor.js.map +1 -1
- package/dist/entry-module.d.ts +0 -8
- package/dist/entry-module.d.ts.map +1 -1
- package/dist/entry-module.js +75 -77
- package/dist/entry-module.js.map +1 -1
- package/dist/execution.d.ts +19 -0
- package/dist/execution.d.ts.map +1 -1
- package/dist/execution.js.map +1 -1
- package/dist/interpreter.d.ts +18 -2
- package/dist/interpreter.d.ts.map +1 -1
- package/dist/interpreter.js +6 -0
- package/dist/interpreter.js.map +1 -1
- package/dist/link.d.ts +7 -0
- package/dist/link.d.ts.map +1 -1
- package/dist/link.js +4 -2
- package/dist/link.js.map +1 -1
- package/dist/playbook-ports.d.ts +11 -1
- package/dist/playbook-ports.d.ts.map +1 -1
- package/dist/playbook-ports.js +8 -1
- package/dist/playbook-ports.js.map +1 -1
- package/dist/reviewing-agent.d.ts +9 -2
- package/dist/reviewing-agent.d.ts.map +1 -1
- package/dist/reviewing-agent.js +232 -38
- package/dist/reviewing-agent.js.map +1 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +171 -32
- package/dist/runner.js.map +1 -1
- package/dist/verify-coverage.d.ts.map +1 -1
- package/dist/verify-coverage.js +35 -0
- package/dist/verify-coverage.js.map +1 -1
- package/dist/verify-source.d.ts +61 -0
- package/dist/verify-source.d.ts.map +1 -0
- package/dist/verify-source.js +280 -0
- package/dist/verify-source.js.map +1 -0
- package/dist/verify.d.ts +26 -1
- package/dist/verify.d.ts.map +1 -1
- package/dist/verify.js +357 -78
- package/dist/verify.js.map +1 -1
- package/package.json +2 -2
package/dist/verify.js
CHANGED
|
@@ -517,6 +517,15 @@ function metadataRole(state) {
|
|
|
517
517
|
return undefined;
|
|
518
518
|
return playbook.role;
|
|
519
519
|
}
|
|
520
|
+
/** The published terminal kind a root final state declares, if any. */
|
|
521
|
+
function metadataTerminal(state) {
|
|
522
|
+
if (typeof state.meta !== 'object' || state.meta === null)
|
|
523
|
+
return undefined;
|
|
524
|
+
const playbook = state.meta.playbook;
|
|
525
|
+
if (typeof playbook !== 'object' || playbook === null)
|
|
526
|
+
return undefined;
|
|
527
|
+
return playbook.terminal;
|
|
528
|
+
}
|
|
520
529
|
function stateIdConsistencyFindings(node, inputStateId) {
|
|
521
530
|
if (!isNonEmptyString(inputStateId))
|
|
522
531
|
return [];
|
|
@@ -554,15 +563,46 @@ function structuredStateIdentityFindings(nodes) {
|
|
|
554
563
|
}
|
|
555
564
|
return findings;
|
|
556
565
|
}
|
|
566
|
+
/**
|
|
567
|
+
* A schema-3 machine publishes each of its own terminal outcomes as
|
|
568
|
+
* `success` or `failure`, so a caller reads the child's meaning from the
|
|
569
|
+
* state it reached rather than from its output fields. Only the machine's own
|
|
570
|
+
* root final states carry that kind: a final inside a parallel region merely
|
|
571
|
+
* stages the join and publishes no workflow outcome.
|
|
572
|
+
*
|
|
573
|
+
* Artifacts compiled before the definition declared the kind carry none at
|
|
574
|
+
* all, and they are retained rather than rebuilt, so the requirement engages
|
|
575
|
+
* once the artifact declares one — a partly declared artifact is drift.
|
|
576
|
+
*/
|
|
577
|
+
function terminalKindFindings(nodes, schema3) {
|
|
578
|
+
if (!schema3)
|
|
579
|
+
return [];
|
|
580
|
+
const finals = nodes.filter(({ path, state }) => path.length === 1 && state.type === 'final');
|
|
581
|
+
if (!finals.some(({ state }) => metadataTerminal(state) !== undefined)) {
|
|
582
|
+
return [];
|
|
583
|
+
}
|
|
584
|
+
const findings = [];
|
|
585
|
+
for (const { statePath, state } of finals) {
|
|
586
|
+
const terminal = metadataTerminal(state);
|
|
587
|
+
if (terminal === undefined) {
|
|
588
|
+
findings.push(`FSM final state ${statePath}: state.meta.playbook.terminal is missing while other final states declare it`);
|
|
589
|
+
}
|
|
590
|
+
else if (terminal !== 'success' && terminal !== 'failure') {
|
|
591
|
+
findings.push(`FSM final state ${statePath}: state.meta.playbook.terminal ${JSON.stringify(terminal)} is neither "success" nor "failure"`);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return findings;
|
|
595
|
+
}
|
|
557
596
|
function enumerateCaptainBindings(config) {
|
|
558
597
|
const out = [];
|
|
598
|
+
const initial = initialMachineContext(config) ?? {};
|
|
559
599
|
for (const node of walkStateNodes(config)) {
|
|
560
600
|
for (const invoke of normalizeInvokes(node.state.invoke)) {
|
|
561
601
|
const source = invokeSource(invoke.src);
|
|
562
602
|
const explicitlyWorkActor = source === 'captain' || source === 'player';
|
|
563
603
|
if (!explicitlyWorkActor && invoke.src !== undefined)
|
|
564
604
|
continue;
|
|
565
|
-
const inspected = invocationInput(invoke);
|
|
605
|
+
const inspected = invocationInput(invoke, initial);
|
|
566
606
|
if ('error' in inspected) {
|
|
567
607
|
if (explicitlyWorkActor) {
|
|
568
608
|
const actor = source === 'player' ? 'player' : 'captain';
|
|
@@ -688,11 +728,12 @@ export function enumerateCaptainStates(config) {
|
|
|
688
728
|
}
|
|
689
729
|
function enumeratePlaybookBindings(config) {
|
|
690
730
|
const out = [];
|
|
731
|
+
const initial = initialMachineContext(config) ?? {};
|
|
691
732
|
for (const node of walkStateNodes(config)) {
|
|
692
733
|
for (const invoke of normalizeInvokes(node.state.invoke)) {
|
|
693
734
|
if (invokeSource(invoke.src) !== 'playbook')
|
|
694
735
|
continue;
|
|
695
|
-
const inspected = invocationInput(invoke);
|
|
736
|
+
const inspected = invocationInput(invoke, initial);
|
|
696
737
|
if ('error' in inspected) {
|
|
697
738
|
out.push({
|
|
698
739
|
state: malformedPlaybookState(publicStateId(node, undefined), `invoke.input threw during introspection: ${inspected.error}`, node),
|
|
@@ -730,6 +771,7 @@ function enumeratePlaybookBindings(config) {
|
|
|
730
771
|
const playbookIdSentinel = sentinelFor(fields.playbookIdContext);
|
|
731
772
|
const textSentinel = sentinelFor(fields.textContext);
|
|
732
773
|
const wired = invocationInput(invoke, {
|
|
774
|
+
...initial,
|
|
733
775
|
[fields.playbookIdContext]: playbookIdSentinel,
|
|
734
776
|
[fields.textContext]: textSentinel,
|
|
735
777
|
});
|
|
@@ -799,6 +841,7 @@ export function enumeratePlaybookStates(config) {
|
|
|
799
841
|
*/
|
|
800
842
|
export function enumerateScriptStates(config) {
|
|
801
843
|
const out = [];
|
|
844
|
+
const initial = initialMachineContext(config) ?? {};
|
|
802
845
|
for (const node of walkStateNodes(config)) {
|
|
803
846
|
for (const invoke of normalizeInvokes(node.state.invoke)) {
|
|
804
847
|
if (invokeSource(invoke.src) !== 'script')
|
|
@@ -811,7 +854,7 @@ export function enumerateScriptStates(config) {
|
|
|
811
854
|
...nestedStatePath(node),
|
|
812
855
|
bindingFindings: [finding],
|
|
813
856
|
});
|
|
814
|
-
const inspected = invocationInput(invoke);
|
|
857
|
+
const inspected = invocationInput(invoke, initial);
|
|
815
858
|
if ('error' in inspected) {
|
|
816
859
|
out.push(malformed(`invoke.input threw during introspection: ${inspected.error}`));
|
|
817
860
|
continue;
|
|
@@ -1020,12 +1063,14 @@ export function checkGearsFsmConformance(gears, config, options = {}) {
|
|
|
1020
1063
|
: `extra ${JSON.stringify(nearMiss.extra[0])}`;
|
|
1021
1064
|
findings.push(`FSM state ${state.stateId}: controller decision contract near-miss (${detail}); the controller domain requires exactly ${CONTROLLER_ACTION_GUARDS.join(', ')}`);
|
|
1022
1065
|
}
|
|
1023
|
-
|
|
1066
|
+
const nodes = walkStateNodes(config);
|
|
1067
|
+
findings.push(...structuredStateIdentityFindings(nodes));
|
|
1024
1068
|
findings.push(...roleContract.findings);
|
|
1025
|
-
const
|
|
1069
|
+
const schema3 = roleContract.generation === 'schema-3' ||
|
|
1026
1070
|
controller ||
|
|
1027
1071
|
options.artifactSchema === 3;
|
|
1028
|
-
|
|
1072
|
+
findings.push(...terminalKindFindings(nodes, schema3));
|
|
1073
|
+
if (schema3) {
|
|
1029
1074
|
const actual = concurrentRoleSets(options.concurrentRoleSets);
|
|
1030
1075
|
if (actual === undefined) {
|
|
1031
1076
|
findings.push('schema-3 FSM exports no valid concurrentRoleSets array');
|
|
@@ -1482,13 +1527,15 @@ const sentinelFor = (field) => `«${field}»`;
|
|
|
1482
1527
|
* Traces which context fields an `invoke.input` thunk reads, via a recording
|
|
1483
1528
|
* proxy context; reads collected up to a throw are kept.
|
|
1484
1529
|
*/
|
|
1485
|
-
export function probeContextReads(inputFn
|
|
1530
|
+
export function probeContextReads(inputFn,
|
|
1531
|
+
/** The machine's initial context, so a typed field does not truncate the trace. */
|
|
1532
|
+
initial = {}) {
|
|
1486
1533
|
const reads = new Set();
|
|
1487
|
-
const context = new Proxy({}, {
|
|
1488
|
-
get(
|
|
1534
|
+
const context = new Proxy({ ...initial }, {
|
|
1535
|
+
get(target, prop) {
|
|
1489
1536
|
if (typeof prop === 'string')
|
|
1490
1537
|
reads.add(prop);
|
|
1491
|
-
return
|
|
1538
|
+
return Reflect.get(target, prop);
|
|
1492
1539
|
},
|
|
1493
1540
|
has() {
|
|
1494
1541
|
return true;
|
|
@@ -1516,6 +1563,117 @@ const BOSS_CONTEXT_FIELDS = [
|
|
|
1516
1563
|
function ordinaryContext(reads) {
|
|
1517
1564
|
return sentinelContext(reads.filter((field) => !BOSS_CONTEXT_FIELDS.includes(field)));
|
|
1518
1565
|
}
|
|
1566
|
+
/**
|
|
1567
|
+
* The machine's resolved initial context (verification-5): a literal record, or
|
|
1568
|
+
* the config's factory resolved with the declared initial input, so a typed
|
|
1569
|
+
* context field an ordinary turn reads keeps its initial shape instead of a
|
|
1570
|
+
* string sentinel. Returns undefined when no initial snapshot can be produced
|
|
1571
|
+
* without running the machine — the caller then degrades to sentinels alone.
|
|
1572
|
+
*/
|
|
1573
|
+
function initialMachineContext(config) {
|
|
1574
|
+
const declared = config.context;
|
|
1575
|
+
if (declared === undefined)
|
|
1576
|
+
return {};
|
|
1577
|
+
if (typeof declared === 'object' && declared !== null) {
|
|
1578
|
+
return { ...declared };
|
|
1579
|
+
}
|
|
1580
|
+
if (typeof declared !== 'function')
|
|
1581
|
+
return undefined;
|
|
1582
|
+
try {
|
|
1583
|
+
const resolved = declared({
|
|
1584
|
+
input: {},
|
|
1585
|
+
spawn: () => undefined,
|
|
1586
|
+
self: undefined,
|
|
1587
|
+
event: { type: 'xstate.init' },
|
|
1588
|
+
});
|
|
1589
|
+
return typeof resolved === 'object' && resolved !== null
|
|
1590
|
+
? { ...resolved }
|
|
1591
|
+
: undefined;
|
|
1592
|
+
}
|
|
1593
|
+
catch {
|
|
1594
|
+
return undefined;
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
/**
|
|
1598
|
+
* The context an ordinary-turn probe drives an `invoke.input` thunk with: the
|
|
1599
|
+
* machine's initial context, overlaid with one string sentinel per traced read
|
|
1600
|
+
* whose initial value is absent or itself a string, and never a Boss-reply
|
|
1601
|
+
* field (verification-5).
|
|
1602
|
+
*/
|
|
1603
|
+
function ordinaryTurnContext(reads, initial,
|
|
1604
|
+
/** Traces the Boss-reply fields too, for wiring capture rather than a turn. */
|
|
1605
|
+
includeBossFields = false) {
|
|
1606
|
+
if (initial === undefined) {
|
|
1607
|
+
return includeBossFields ? sentinelContext(reads) : ordinaryContext(reads);
|
|
1608
|
+
}
|
|
1609
|
+
const context = { ...initial };
|
|
1610
|
+
if (!includeBossFields) {
|
|
1611
|
+
for (const field of BOSS_CONTEXT_FIELDS)
|
|
1612
|
+
delete context[field];
|
|
1613
|
+
}
|
|
1614
|
+
for (const field of reads) {
|
|
1615
|
+
if (!includeBossFields && BOSS_CONTEXT_FIELDS.includes(field))
|
|
1616
|
+
continue;
|
|
1617
|
+
const current = context[field];
|
|
1618
|
+
if (current === undefined || typeof current === 'string') {
|
|
1619
|
+
context[field] = sentinelFor(field);
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
return context;
|
|
1623
|
+
}
|
|
1624
|
+
// Input fields the composition contract itself owns: a placeholder naming one
|
|
1625
|
+
// of them relays no runtime value, so the probe never overwrites it.
|
|
1626
|
+
const CONTRACT_INPUT_FIELDS = [
|
|
1627
|
+
'prompt',
|
|
1628
|
+
'result',
|
|
1629
|
+
'stateId',
|
|
1630
|
+
'sourceItem',
|
|
1631
|
+
'role',
|
|
1632
|
+
'player',
|
|
1633
|
+
...BOSS_CONTEXT_FIELDS,
|
|
1634
|
+
];
|
|
1635
|
+
/** The canonical field a `<kebab-token>` placeholder relays: its segments joined camel-case. */
|
|
1636
|
+
function placeholderField(token) {
|
|
1637
|
+
const segments = token.replace(/^<|>$/g, '').split('-');
|
|
1638
|
+
return segments
|
|
1639
|
+
.map((segment, index) => index === 0
|
|
1640
|
+
? segment
|
|
1641
|
+
: segment.charAt(0).toUpperCase() + segment.slice(1))
|
|
1642
|
+
.join('');
|
|
1643
|
+
}
|
|
1644
|
+
/**
|
|
1645
|
+
* Overlays one string sentinel on every placeholder-derived input field the
|
|
1646
|
+
* prompt relays, so a field the machine's initial context leaves empty or
|
|
1647
|
+
* derives through a typed value still evidences substitution (verification-5).
|
|
1648
|
+
* Returns the probed input and the sentinel field names it carries.
|
|
1649
|
+
*/
|
|
1650
|
+
function withPlaceholderValues(state, input) {
|
|
1651
|
+
if (typeof input !== 'object' || input === null)
|
|
1652
|
+
return { input, fields: [] };
|
|
1653
|
+
const record = input;
|
|
1654
|
+
const probed = { ...record };
|
|
1655
|
+
const fields = [];
|
|
1656
|
+
for (const token of placeholdersIn(state.prompt)) {
|
|
1657
|
+
const field = placeholderField(token);
|
|
1658
|
+
if (field === '' || CONTRACT_INPUT_FIELDS.includes(field))
|
|
1659
|
+
continue;
|
|
1660
|
+
if (typeof record[field] !== 'string')
|
|
1661
|
+
continue;
|
|
1662
|
+
probed[field] = sentinelFor(field);
|
|
1663
|
+
if (!fields.includes(field))
|
|
1664
|
+
fields.push(field);
|
|
1665
|
+
}
|
|
1666
|
+
return { input: probed, fields };
|
|
1667
|
+
}
|
|
1668
|
+
/** Every canonical local role the artifact's player states declare (verification-1). */
|
|
1669
|
+
function declaredLocalRoles(config) {
|
|
1670
|
+
const roles = new Set();
|
|
1671
|
+
for (const state of enumerateCaptainStates(config)) {
|
|
1672
|
+
if (state.role !== undefined)
|
|
1673
|
+
roles.add(state.role);
|
|
1674
|
+
}
|
|
1675
|
+
return [...roles].sort();
|
|
1676
|
+
}
|
|
1519
1677
|
function carriesSentinel(value, sentinel) {
|
|
1520
1678
|
try {
|
|
1521
1679
|
return (JSON.stringify(value) ?? '').includes(sentinel);
|
|
@@ -1531,14 +1689,17 @@ function carriesSentinel(value, sentinel) {
|
|
|
1531
1689
|
*/
|
|
1532
1690
|
export function capturePromptContract(config) {
|
|
1533
1691
|
const rows = [];
|
|
1692
|
+
const initial = initialMachineContext(config);
|
|
1534
1693
|
for (const binding of enumerateCaptainBindings(config)) {
|
|
1535
1694
|
const { state, inputFn } = binding;
|
|
1536
1695
|
if (typeof inputFn !== 'function')
|
|
1537
1696
|
continue;
|
|
1538
|
-
const reads = probeContextReads(inputFn);
|
|
1697
|
+
const reads = probeContextReads(inputFn, initial);
|
|
1539
1698
|
const wires = {};
|
|
1540
1699
|
try {
|
|
1541
|
-
const input = inputFn({
|
|
1700
|
+
const input = inputFn({
|
|
1701
|
+
context: ordinaryTurnContext(reads, initial, true),
|
|
1702
|
+
});
|
|
1542
1703
|
if (typeof input === 'object' && input !== null) {
|
|
1543
1704
|
for (const [key, value] of Object.entries(input)) {
|
|
1544
1705
|
const carried = reads.filter((field) => carriesSentinel(value, sentinelFor(field)));
|
|
@@ -1570,6 +1731,8 @@ export function capturePromptContract(config) {
|
|
|
1570
1731
|
*/
|
|
1571
1732
|
export function deriveSubstitutions(config, compose, actor) {
|
|
1572
1733
|
const out = {};
|
|
1734
|
+
const roles = declaredLocalRoles(config);
|
|
1735
|
+
const initial = initialMachineContext(config);
|
|
1573
1736
|
for (const binding of enumerateCaptainBindings(config)) {
|
|
1574
1737
|
const { state, inputFn } = binding;
|
|
1575
1738
|
if (actor !== undefined && state.actor !== actor)
|
|
@@ -1577,8 +1740,9 @@ export function deriveSubstitutions(config, compose, actor) {
|
|
|
1577
1740
|
if (typeof inputFn !== 'function')
|
|
1578
1741
|
continue;
|
|
1579
1742
|
try {
|
|
1580
|
-
const reads = probeContextReads(inputFn);
|
|
1581
|
-
const
|
|
1743
|
+
const reads = probeContextReads(inputFn, initial);
|
|
1744
|
+
const probed = withPlaceholderValues(state, inputFn({ context: ordinaryTurnContext(reads, initial) }));
|
|
1745
|
+
const composed = composeForState(compose, state, probed.input, roles);
|
|
1582
1746
|
if (typeof composed !== 'string') {
|
|
1583
1747
|
out[state.stateId] = [];
|
|
1584
1748
|
continue;
|
|
@@ -1589,7 +1753,7 @@ export function deriveSubstitutions(config, compose, actor) {
|
|
|
1589
1753
|
// hide valid evidence, while merely deleting a token still cannot
|
|
1590
1754
|
// masquerade as substitution.
|
|
1591
1755
|
const evidenced = new Set();
|
|
1592
|
-
const promptReads = promptSentinelFields(state, reads);
|
|
1756
|
+
const promptReads = promptSentinelFields(state, reads, probed.fields, roles);
|
|
1593
1757
|
for (const line of state.prompt.split('\n')) {
|
|
1594
1758
|
for (const token of matchPromptBody(line, composed, promptReads)
|
|
1595
1759
|
?.substitutions ?? []) {
|
|
@@ -1615,6 +1779,11 @@ export function deriveSubstitutions(config, compose, actor) {
|
|
|
1615
1779
|
export function checkPromptComposition(opts) {
|
|
1616
1780
|
const findings = [];
|
|
1617
1781
|
const controller = isControllerMachine(opts.config);
|
|
1782
|
+
const roles = declaredLocalRoles(opts.config);
|
|
1783
|
+
const initial = initialMachineContext(opts.config);
|
|
1784
|
+
if (initial === undefined) {
|
|
1785
|
+
opts.diagnostics?.push('the machine has no resolvable initial context; ordinary-turn input is synthesized from context sentinels alone');
|
|
1786
|
+
}
|
|
1618
1787
|
const schemaResolution = resolveArtifactSchemaForVerification({
|
|
1619
1788
|
config: opts.config,
|
|
1620
1789
|
...(opts.artifactSchema === undefined
|
|
@@ -1632,12 +1801,14 @@ export function checkPromptComposition(opts) {
|
|
|
1632
1801
|
continue;
|
|
1633
1802
|
if (typeof inputFn !== 'function')
|
|
1634
1803
|
continue;
|
|
1635
|
-
const reads = probeContextReads(inputFn);
|
|
1636
|
-
const promptReads = promptSentinelFields(state, reads);
|
|
1804
|
+
const reads = probeContextReads(inputFn, initial);
|
|
1637
1805
|
const substituted = substitutions[state.stateId] ?? [];
|
|
1638
1806
|
let ordinary;
|
|
1807
|
+
let promptReads;
|
|
1639
1808
|
try {
|
|
1640
|
-
|
|
1809
|
+
const probed = withPlaceholderValues(state, inputFn({ context: ordinaryTurnContext(reads, initial) }));
|
|
1810
|
+
promptReads = promptSentinelFields(state, reads, probed.fields, roles);
|
|
1811
|
+
ordinary = composeForState(opts.compose, state, probed.input, roles);
|
|
1641
1812
|
if (typeof ordinary !== 'string') {
|
|
1642
1813
|
throw new Error(`${composerName} returned a non-string value`);
|
|
1643
1814
|
}
|
|
@@ -1692,9 +1863,9 @@ export function checkPromptComposition(opts) {
|
|
|
1692
1863
|
let continuation;
|
|
1693
1864
|
let input;
|
|
1694
1865
|
try {
|
|
1695
|
-
|
|
1866
|
+
const probed = withPlaceholderValues(state, inputFn({
|
|
1696
1867
|
context: {
|
|
1697
|
-
...
|
|
1868
|
+
...ordinaryTurnContext(reads, initial),
|
|
1698
1869
|
pendingBossQuestion,
|
|
1699
1870
|
bossReply: reply,
|
|
1700
1871
|
pendingBossQuestions: {
|
|
@@ -1702,8 +1873,10 @@ export function checkPromptComposition(opts) {
|
|
|
1702
1873
|
},
|
|
1703
1874
|
bossReplies: { [state.stateId]: reply },
|
|
1704
1875
|
},
|
|
1705
|
-
});
|
|
1706
|
-
|
|
1876
|
+
}));
|
|
1877
|
+
input = probed.input;
|
|
1878
|
+
promptReads = promptSentinelFields(state, reads, probed.fields, roles);
|
|
1879
|
+
continuation = composeForState(opts.compose, state, input, roles);
|
|
1707
1880
|
if (typeof continuation !== 'string') {
|
|
1708
1881
|
throw new Error(`${composerName} returned a non-string value`);
|
|
1709
1882
|
}
|
|
@@ -1746,12 +1919,13 @@ export function checkPromptComposition(opts) {
|
|
|
1746
1919
|
}
|
|
1747
1920
|
return findings;
|
|
1748
1921
|
}
|
|
1749
|
-
function promptSentinelFields(state, reads) {
|
|
1922
|
+
function promptSentinelFields(state, reads, placeholderFields, declaredRoles) {
|
|
1923
|
+
const fields = [...reads, ...placeholderFields];
|
|
1750
1924
|
return state.role === undefined
|
|
1751
|
-
?
|
|
1752
|
-
: [...
|
|
1925
|
+
? fields
|
|
1926
|
+
: [...fields, ...declaredRoles.map((role) => `promptIdentity:${role}`)];
|
|
1753
1927
|
}
|
|
1754
|
-
function composeForState(compose, state, input) {
|
|
1928
|
+
function composeForState(compose, state, input, declaredRoles) {
|
|
1755
1929
|
// Schema-1 composers and the shared default composer use their second
|
|
1756
1930
|
// positional argument as a placeholder-field map. A callable proxy with a
|
|
1757
1931
|
// property-clean view is therefore both an invocation-scoped schema-3 lookup
|
|
@@ -1761,8 +1935,11 @@ function composeForState(compose, state, input) {
|
|
|
1761
1935
|
if (state.role === undefined) {
|
|
1762
1936
|
throw new Error(`prompt identity lookup used role ${JSON.stringify(roleId)} for a direct-Captain or historical state`);
|
|
1763
1937
|
}
|
|
1764
|
-
|
|
1765
|
-
|
|
1938
|
+
// A prompt may name any declared role's identity — a Coder prompt reaches
|
|
1939
|
+
// the Reviewer's `<reviewer-llm>` — so only an undeclared role is drift
|
|
1940
|
+
// (link.md, "Player prompt composition").
|
|
1941
|
+
if (!declaredRoles.includes(roleId)) {
|
|
1942
|
+
throw new Error(`prompt identity lookup used undeclared role ${JSON.stringify(roleId)}; the artifact declares ${JSON.stringify(declaredRoles)}`);
|
|
1766
1943
|
}
|
|
1767
1944
|
return sentinelFor(`promptIdentity:${roleId}`);
|
|
1768
1945
|
};
|
|
@@ -2316,6 +2493,135 @@ export function resolveArtifactSchemaForVerification(opts) {
|
|
|
2316
2493
|
}
|
|
2317
2494
|
: { findings: [] };
|
|
2318
2495
|
}
|
|
2496
|
+
/** The `_internal` composer export serving one state actor (verification-5). */
|
|
2497
|
+
const COMPOSER_EXPORTS = {
|
|
2498
|
+
captain: 'composeCaptainPrompt',
|
|
2499
|
+
player: 'composePlayerPrompt',
|
|
2500
|
+
};
|
|
2501
|
+
/**
|
|
2502
|
+
* Runs the deterministic prompt-contract checks over one live linked module
|
|
2503
|
+
* beside its FSM (verification-5, verification-27): the module imports through
|
|
2504
|
+
* the ephemeral TypeScript FSM edge, its schema evidence reconciles, and every
|
|
2505
|
+
* actor the machine invokes has a composer that honors the link contract.
|
|
2506
|
+
*
|
|
2507
|
+
* Shared by the emitted suite's generator, which pins the derived substitutions
|
|
2508
|
+
* and degrades each observation to a diagnostic, and by the link-fidelity gate,
|
|
2509
|
+
* which relays the same observations as findings — so the gate adds no check of
|
|
2510
|
+
* its own (DR-030).
|
|
2511
|
+
*/
|
|
2512
|
+
async function inspectLinkedPromptContract(opts) {
|
|
2513
|
+
const { config, evidence } = opts;
|
|
2514
|
+
const withoutModule = (extra) => {
|
|
2515
|
+
const resolution = resolveArtifactSchemaForVerification({
|
|
2516
|
+
config,
|
|
2517
|
+
...evidence,
|
|
2518
|
+
});
|
|
2519
|
+
return {
|
|
2520
|
+
...(resolution.artifactSchema === undefined
|
|
2521
|
+
? {}
|
|
2522
|
+
: { artifactSchema: resolution.artifactSchema }),
|
|
2523
|
+
schemaFindings: resolution.findings,
|
|
2524
|
+
present: true,
|
|
2525
|
+
composers: [],
|
|
2526
|
+
...extra,
|
|
2527
|
+
};
|
|
2528
|
+
};
|
|
2529
|
+
if (!existsSync(opts.linkedPath))
|
|
2530
|
+
return withoutModule({ present: false });
|
|
2531
|
+
let linked;
|
|
2532
|
+
try {
|
|
2533
|
+
linked = (await loadLinkedModuleForVerification({
|
|
2534
|
+
linkedPath: opts.linkedPath,
|
|
2535
|
+
fsmPath: opts.fsmPath,
|
|
2536
|
+
}));
|
|
2537
|
+
}
|
|
2538
|
+
catch (error) {
|
|
2539
|
+
return withoutModule({ loadError: messageOf(error) });
|
|
2540
|
+
}
|
|
2541
|
+
const resolution = resolveArtifactSchemaForVerification({
|
|
2542
|
+
config,
|
|
2543
|
+
linked,
|
|
2544
|
+
...evidence,
|
|
2545
|
+
});
|
|
2546
|
+
const artifactSchema = resolution.artifactSchema;
|
|
2547
|
+
const actors = new Set(enumerateCaptainStates(config).map(({ actor }) => actor));
|
|
2548
|
+
const composers = [];
|
|
2549
|
+
for (const actor of ['captain', 'player']) {
|
|
2550
|
+
if (!actors.has(actor))
|
|
2551
|
+
continue;
|
|
2552
|
+
const exportName = COMPOSER_EXPORTS[actor];
|
|
2553
|
+
const compose = linked._internal?.[exportName];
|
|
2554
|
+
if (typeof compose !== 'function') {
|
|
2555
|
+
composers.push({ actor, exportName, findings: [], diagnostics: [] });
|
|
2556
|
+
continue;
|
|
2557
|
+
}
|
|
2558
|
+
const typedCompose = compose;
|
|
2559
|
+
const diagnostics = [];
|
|
2560
|
+
composers.push({
|
|
2561
|
+
actor,
|
|
2562
|
+
exportName,
|
|
2563
|
+
substitutions: deriveSubstitutions(config, typedCompose, actor),
|
|
2564
|
+
findings: checkPromptComposition({
|
|
2565
|
+
config,
|
|
2566
|
+
compose: typedCompose,
|
|
2567
|
+
actor,
|
|
2568
|
+
...(artifactSchema === undefined ? {} : { artifactSchema }),
|
|
2569
|
+
diagnostics,
|
|
2570
|
+
}),
|
|
2571
|
+
diagnostics,
|
|
2572
|
+
});
|
|
2573
|
+
}
|
|
2574
|
+
return {
|
|
2575
|
+
...(artifactSchema === undefined ? {} : { artifactSchema }),
|
|
2576
|
+
schemaFindings: resolution.findings,
|
|
2577
|
+
present: true,
|
|
2578
|
+
composers,
|
|
2579
|
+
};
|
|
2580
|
+
}
|
|
2581
|
+
/**
|
|
2582
|
+
* The deterministic link-fidelity checks over one live linked `playbook` module
|
|
2583
|
+
* beside its FSM (verification-27): exactly the checks the emitted
|
|
2584
|
+
* prompt-contract suite asserts, decided from those two paths alone and
|
|
2585
|
+
* returning one finding per violation.
|
|
2586
|
+
*
|
|
2587
|
+
* The gate that runs this before any Reviewer call must never throw into the
|
|
2588
|
+
* executor it guards (DR-030, phase-execution-53), so a module that cannot be
|
|
2589
|
+
* imported yields its import diagnostic as a finding. An absent module or FSM
|
|
2590
|
+
* yields none: an artifact the performing call has not written is the generic
|
|
2591
|
+
* target check's business (phase-execution-4). An FSM that cannot be imported
|
|
2592
|
+
* yields none either — the suite's checks derive from that machine, so they
|
|
2593
|
+
* degrade exactly as emission degrades its prompt-contract test to a
|
|
2594
|
+
* diagnostic (verification-5).
|
|
2595
|
+
*/
|
|
2596
|
+
export async function checkLinkedModuleContract(opts) {
|
|
2597
|
+
if (!existsSync(opts.linkedPath) || !existsSync(opts.fsmPath))
|
|
2598
|
+
return [];
|
|
2599
|
+
let config;
|
|
2600
|
+
try {
|
|
2601
|
+
config = findMachineConfig(await loadFsmModule(opts.fsmPath));
|
|
2602
|
+
}
|
|
2603
|
+
catch {
|
|
2604
|
+
return [];
|
|
2605
|
+
}
|
|
2606
|
+
const contract = await inspectLinkedPromptContract({
|
|
2607
|
+
config,
|
|
2608
|
+
linkedPath: opts.linkedPath,
|
|
2609
|
+
fsmPath: opts.fsmPath,
|
|
2610
|
+
evidence: {},
|
|
2611
|
+
});
|
|
2612
|
+
const findings = [...contract.schemaFindings];
|
|
2613
|
+
if (contract.loadError !== undefined) {
|
|
2614
|
+
findings.push(`linked module could not be imported: ${contract.loadError}`);
|
|
2615
|
+
return findings;
|
|
2616
|
+
}
|
|
2617
|
+
// A module exposing no matching composer degrades exactly as emission does
|
|
2618
|
+
// (verification-5): the suite asserts nothing about it, so neither does the
|
|
2619
|
+
// gate that reuses the suite's checks.
|
|
2620
|
+
for (const composer of contract.composers) {
|
|
2621
|
+
findings.push(...composer.findings);
|
|
2622
|
+
}
|
|
2623
|
+
return findings;
|
|
2624
|
+
}
|
|
2319
2625
|
/**
|
|
2320
2626
|
* Emits the prompt-contract test beside a compiled `playbook` artifact
|
|
2321
2627
|
* (verification-5): derives and pins the per-state contract from the physical
|
|
@@ -2344,66 +2650,39 @@ export async function emitPromptContractTest(opts) {
|
|
|
2344
2650
|
? {}
|
|
2345
2651
|
: { artifactSchema: opts.artifactSchema }),
|
|
2346
2652
|
};
|
|
2347
|
-
|
|
2653
|
+
const contract = await inspectLinkedPromptContract({
|
|
2348
2654
|
config,
|
|
2349
|
-
|
|
2655
|
+
linkedPath: join(opts.artifactDir, `${opts.basename}.playbook.ts`),
|
|
2656
|
+
fsmPath,
|
|
2657
|
+
evidence,
|
|
2350
2658
|
});
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
schemaResolution = resolveArtifactSchemaForVerification({
|
|
2361
|
-
config,
|
|
2362
|
-
linked,
|
|
2363
|
-
...evidence,
|
|
2364
|
-
});
|
|
2365
|
-
artifactSchema = schemaResolution.artifactSchema;
|
|
2366
|
-
const actors = new Set(enumerateCaptainStates(config).map(({ actor }) => actor));
|
|
2367
|
-
const substitutions = {};
|
|
2368
|
-
for (const actor of ['captain', 'player']) {
|
|
2369
|
-
if (!actors.has(actor))
|
|
2370
|
-
continue;
|
|
2371
|
-
const exportName = actor === 'captain' ? 'composeCaptainPrompt' : 'composePlayerPrompt';
|
|
2372
|
-
const compose = linked._internal?.[exportName];
|
|
2373
|
-
if (typeof compose !== 'function') {
|
|
2374
|
-
diagnostics.push(`prompt contract: linked module exposes no _internal.${exportName}; ${actor} composition checks not emitted`);
|
|
2375
|
-
continue;
|
|
2376
|
-
}
|
|
2377
|
-
const typedCompose = compose;
|
|
2378
|
-
substitutions[actor] = deriveSubstitutions(config, typedCompose, actor);
|
|
2379
|
-
const findings = checkPromptComposition({
|
|
2380
|
-
config,
|
|
2381
|
-
compose: typedCompose,
|
|
2382
|
-
actor,
|
|
2383
|
-
...(artifactSchema === undefined ? {} : { artifactSchema }),
|
|
2384
|
-
});
|
|
2385
|
-
diagnostics.push(...findings.map((finding) => `prompt contract: ${finding}`));
|
|
2386
|
-
}
|
|
2387
|
-
if (substitutions.captain !== undefined ||
|
|
2388
|
-
substitutions.player !== undefined) {
|
|
2389
|
-
composer = {
|
|
2390
|
-
playbookModule: `./${opts.basename}.playbook.js`,
|
|
2391
|
-
...substitutions,
|
|
2392
|
-
};
|
|
2393
|
-
}
|
|
2394
|
-
}
|
|
2395
|
-
catch (error) {
|
|
2396
|
-
diagnostics.push(`prompt contract: linked module could not be imported (${messageOf(error)}); composition checks not emitted`);
|
|
2659
|
+
const artifactSchema = contract.artifactSchema;
|
|
2660
|
+
if (contract.loadError !== undefined) {
|
|
2661
|
+
diagnostics.push(`prompt contract: linked module could not be imported (${contract.loadError}); composition checks not emitted`);
|
|
2662
|
+
}
|
|
2663
|
+
const substitutions = {};
|
|
2664
|
+
for (const inspected of contract.composers) {
|
|
2665
|
+
if (inspected.substitutions === undefined) {
|
|
2666
|
+
diagnostics.push(`prompt contract: linked module exposes no _internal.${inspected.exportName}; ${inspected.actor} composition checks not emitted`);
|
|
2667
|
+
continue;
|
|
2397
2668
|
}
|
|
2669
|
+
substitutions[inspected.actor] = inspected.substitutions;
|
|
2670
|
+
diagnostics.push(...inspected.diagnostics.map((diagnostic) => `prompt contract: ${diagnostic}`), ...inspected.findings.map((finding) => `prompt contract: ${finding}`));
|
|
2398
2671
|
}
|
|
2399
|
-
|
|
2672
|
+
const composer = substitutions.captain !== undefined || substitutions.player !== undefined
|
|
2673
|
+
? {
|
|
2674
|
+
playbookModule: `./${opts.basename}.playbook.js`,
|
|
2675
|
+
...substitutions,
|
|
2676
|
+
}
|
|
2677
|
+
: undefined;
|
|
2678
|
+
diagnostics.unshift(...contract.schemaFindings.map((finding) => `prompt contract: ${finding}`));
|
|
2400
2679
|
const content = generatePromptContractTest({
|
|
2401
2680
|
basename: opts.basename,
|
|
2402
2681
|
fsmModule: `./${opts.basename}.fsm.js`,
|
|
2403
2682
|
verifyModule: opts.verifyModule ?? VERIFY_MODULE,
|
|
2404
2683
|
rows,
|
|
2405
2684
|
...(artifactSchema === undefined ? {} : { artifactSchema }),
|
|
2406
|
-
schemaFindings:
|
|
2685
|
+
schemaFindings: contract.schemaFindings,
|
|
2407
2686
|
composer,
|
|
2408
2687
|
});
|
|
2409
2688
|
await mkdir(opts.artifactDir, { recursive: true });
|