@sublang/slc 0.7.0 → 0.8.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-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 +313 -75
- package/dist/verify.js.map +1 -1
- package/package.json +2 -2
package/dist/verify.js
CHANGED
|
@@ -556,13 +556,14 @@ function structuredStateIdentityFindings(nodes) {
|
|
|
556
556
|
}
|
|
557
557
|
function enumerateCaptainBindings(config) {
|
|
558
558
|
const out = [];
|
|
559
|
+
const initial = initialMachineContext(config) ?? {};
|
|
559
560
|
for (const node of walkStateNodes(config)) {
|
|
560
561
|
for (const invoke of normalizeInvokes(node.state.invoke)) {
|
|
561
562
|
const source = invokeSource(invoke.src);
|
|
562
563
|
const explicitlyWorkActor = source === 'captain' || source === 'player';
|
|
563
564
|
if (!explicitlyWorkActor && invoke.src !== undefined)
|
|
564
565
|
continue;
|
|
565
|
-
const inspected = invocationInput(invoke);
|
|
566
|
+
const inspected = invocationInput(invoke, initial);
|
|
566
567
|
if ('error' in inspected) {
|
|
567
568
|
if (explicitlyWorkActor) {
|
|
568
569
|
const actor = source === 'player' ? 'player' : 'captain';
|
|
@@ -688,11 +689,12 @@ export function enumerateCaptainStates(config) {
|
|
|
688
689
|
}
|
|
689
690
|
function enumeratePlaybookBindings(config) {
|
|
690
691
|
const out = [];
|
|
692
|
+
const initial = initialMachineContext(config) ?? {};
|
|
691
693
|
for (const node of walkStateNodes(config)) {
|
|
692
694
|
for (const invoke of normalizeInvokes(node.state.invoke)) {
|
|
693
695
|
if (invokeSource(invoke.src) !== 'playbook')
|
|
694
696
|
continue;
|
|
695
|
-
const inspected = invocationInput(invoke);
|
|
697
|
+
const inspected = invocationInput(invoke, initial);
|
|
696
698
|
if ('error' in inspected) {
|
|
697
699
|
out.push({
|
|
698
700
|
state: malformedPlaybookState(publicStateId(node, undefined), `invoke.input threw during introspection: ${inspected.error}`, node),
|
|
@@ -730,6 +732,7 @@ function enumeratePlaybookBindings(config) {
|
|
|
730
732
|
const playbookIdSentinel = sentinelFor(fields.playbookIdContext);
|
|
731
733
|
const textSentinel = sentinelFor(fields.textContext);
|
|
732
734
|
const wired = invocationInput(invoke, {
|
|
735
|
+
...initial,
|
|
733
736
|
[fields.playbookIdContext]: playbookIdSentinel,
|
|
734
737
|
[fields.textContext]: textSentinel,
|
|
735
738
|
});
|
|
@@ -799,6 +802,7 @@ export function enumeratePlaybookStates(config) {
|
|
|
799
802
|
*/
|
|
800
803
|
export function enumerateScriptStates(config) {
|
|
801
804
|
const out = [];
|
|
805
|
+
const initial = initialMachineContext(config) ?? {};
|
|
802
806
|
for (const node of walkStateNodes(config)) {
|
|
803
807
|
for (const invoke of normalizeInvokes(node.state.invoke)) {
|
|
804
808
|
if (invokeSource(invoke.src) !== 'script')
|
|
@@ -811,7 +815,7 @@ export function enumerateScriptStates(config) {
|
|
|
811
815
|
...nestedStatePath(node),
|
|
812
816
|
bindingFindings: [finding],
|
|
813
817
|
});
|
|
814
|
-
const inspected = invocationInput(invoke);
|
|
818
|
+
const inspected = invocationInput(invoke, initial);
|
|
815
819
|
if ('error' in inspected) {
|
|
816
820
|
out.push(malformed(`invoke.input threw during introspection: ${inspected.error}`));
|
|
817
821
|
continue;
|
|
@@ -1482,13 +1486,15 @@ const sentinelFor = (field) => `«${field}»`;
|
|
|
1482
1486
|
* Traces which context fields an `invoke.input` thunk reads, via a recording
|
|
1483
1487
|
* proxy context; reads collected up to a throw are kept.
|
|
1484
1488
|
*/
|
|
1485
|
-
export function probeContextReads(inputFn
|
|
1489
|
+
export function probeContextReads(inputFn,
|
|
1490
|
+
/** The machine's initial context, so a typed field does not truncate the trace. */
|
|
1491
|
+
initial = {}) {
|
|
1486
1492
|
const reads = new Set();
|
|
1487
|
-
const context = new Proxy({}, {
|
|
1488
|
-
get(
|
|
1493
|
+
const context = new Proxy({ ...initial }, {
|
|
1494
|
+
get(target, prop) {
|
|
1489
1495
|
if (typeof prop === 'string')
|
|
1490
1496
|
reads.add(prop);
|
|
1491
|
-
return
|
|
1497
|
+
return Reflect.get(target, prop);
|
|
1492
1498
|
},
|
|
1493
1499
|
has() {
|
|
1494
1500
|
return true;
|
|
@@ -1516,6 +1522,117 @@ const BOSS_CONTEXT_FIELDS = [
|
|
|
1516
1522
|
function ordinaryContext(reads) {
|
|
1517
1523
|
return sentinelContext(reads.filter((field) => !BOSS_CONTEXT_FIELDS.includes(field)));
|
|
1518
1524
|
}
|
|
1525
|
+
/**
|
|
1526
|
+
* The machine's resolved initial context (verification-5): a literal record, or
|
|
1527
|
+
* the config's factory resolved with the declared initial input, so a typed
|
|
1528
|
+
* context field an ordinary turn reads keeps its initial shape instead of a
|
|
1529
|
+
* string sentinel. Returns undefined when no initial snapshot can be produced
|
|
1530
|
+
* without running the machine — the caller then degrades to sentinels alone.
|
|
1531
|
+
*/
|
|
1532
|
+
function initialMachineContext(config) {
|
|
1533
|
+
const declared = config.context;
|
|
1534
|
+
if (declared === undefined)
|
|
1535
|
+
return {};
|
|
1536
|
+
if (typeof declared === 'object' && declared !== null) {
|
|
1537
|
+
return { ...declared };
|
|
1538
|
+
}
|
|
1539
|
+
if (typeof declared !== 'function')
|
|
1540
|
+
return undefined;
|
|
1541
|
+
try {
|
|
1542
|
+
const resolved = declared({
|
|
1543
|
+
input: {},
|
|
1544
|
+
spawn: () => undefined,
|
|
1545
|
+
self: undefined,
|
|
1546
|
+
event: { type: 'xstate.init' },
|
|
1547
|
+
});
|
|
1548
|
+
return typeof resolved === 'object' && resolved !== null
|
|
1549
|
+
? { ...resolved }
|
|
1550
|
+
: undefined;
|
|
1551
|
+
}
|
|
1552
|
+
catch {
|
|
1553
|
+
return undefined;
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
/**
|
|
1557
|
+
* The context an ordinary-turn probe drives an `invoke.input` thunk with: the
|
|
1558
|
+
* machine's initial context, overlaid with one string sentinel per traced read
|
|
1559
|
+
* whose initial value is absent or itself a string, and never a Boss-reply
|
|
1560
|
+
* field (verification-5).
|
|
1561
|
+
*/
|
|
1562
|
+
function ordinaryTurnContext(reads, initial,
|
|
1563
|
+
/** Traces the Boss-reply fields too, for wiring capture rather than a turn. */
|
|
1564
|
+
includeBossFields = false) {
|
|
1565
|
+
if (initial === undefined) {
|
|
1566
|
+
return includeBossFields ? sentinelContext(reads) : ordinaryContext(reads);
|
|
1567
|
+
}
|
|
1568
|
+
const context = { ...initial };
|
|
1569
|
+
if (!includeBossFields) {
|
|
1570
|
+
for (const field of BOSS_CONTEXT_FIELDS)
|
|
1571
|
+
delete context[field];
|
|
1572
|
+
}
|
|
1573
|
+
for (const field of reads) {
|
|
1574
|
+
if (!includeBossFields && BOSS_CONTEXT_FIELDS.includes(field))
|
|
1575
|
+
continue;
|
|
1576
|
+
const current = context[field];
|
|
1577
|
+
if (current === undefined || typeof current === 'string') {
|
|
1578
|
+
context[field] = sentinelFor(field);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
return context;
|
|
1582
|
+
}
|
|
1583
|
+
// Input fields the composition contract itself owns: a placeholder naming one
|
|
1584
|
+
// of them relays no runtime value, so the probe never overwrites it.
|
|
1585
|
+
const CONTRACT_INPUT_FIELDS = [
|
|
1586
|
+
'prompt',
|
|
1587
|
+
'result',
|
|
1588
|
+
'stateId',
|
|
1589
|
+
'sourceItem',
|
|
1590
|
+
'role',
|
|
1591
|
+
'player',
|
|
1592
|
+
...BOSS_CONTEXT_FIELDS,
|
|
1593
|
+
];
|
|
1594
|
+
/** The canonical field a `<kebab-token>` placeholder relays: its segments joined camel-case. */
|
|
1595
|
+
function placeholderField(token) {
|
|
1596
|
+
const segments = token.replace(/^<|>$/g, '').split('-');
|
|
1597
|
+
return segments
|
|
1598
|
+
.map((segment, index) => index === 0
|
|
1599
|
+
? segment
|
|
1600
|
+
: segment.charAt(0).toUpperCase() + segment.slice(1))
|
|
1601
|
+
.join('');
|
|
1602
|
+
}
|
|
1603
|
+
/**
|
|
1604
|
+
* Overlays one string sentinel on every placeholder-derived input field the
|
|
1605
|
+
* prompt relays, so a field the machine's initial context leaves empty or
|
|
1606
|
+
* derives through a typed value still evidences substitution (verification-5).
|
|
1607
|
+
* Returns the probed input and the sentinel field names it carries.
|
|
1608
|
+
*/
|
|
1609
|
+
function withPlaceholderValues(state, input) {
|
|
1610
|
+
if (typeof input !== 'object' || input === null)
|
|
1611
|
+
return { input, fields: [] };
|
|
1612
|
+
const record = input;
|
|
1613
|
+
const probed = { ...record };
|
|
1614
|
+
const fields = [];
|
|
1615
|
+
for (const token of placeholdersIn(state.prompt)) {
|
|
1616
|
+
const field = placeholderField(token);
|
|
1617
|
+
if (field === '' || CONTRACT_INPUT_FIELDS.includes(field))
|
|
1618
|
+
continue;
|
|
1619
|
+
if (typeof record[field] !== 'string')
|
|
1620
|
+
continue;
|
|
1621
|
+
probed[field] = sentinelFor(field);
|
|
1622
|
+
if (!fields.includes(field))
|
|
1623
|
+
fields.push(field);
|
|
1624
|
+
}
|
|
1625
|
+
return { input: probed, fields };
|
|
1626
|
+
}
|
|
1627
|
+
/** Every canonical local role the artifact's player states declare (verification-1). */
|
|
1628
|
+
function declaredLocalRoles(config) {
|
|
1629
|
+
const roles = new Set();
|
|
1630
|
+
for (const state of enumerateCaptainStates(config)) {
|
|
1631
|
+
if (state.role !== undefined)
|
|
1632
|
+
roles.add(state.role);
|
|
1633
|
+
}
|
|
1634
|
+
return [...roles].sort();
|
|
1635
|
+
}
|
|
1519
1636
|
function carriesSentinel(value, sentinel) {
|
|
1520
1637
|
try {
|
|
1521
1638
|
return (JSON.stringify(value) ?? '').includes(sentinel);
|
|
@@ -1531,14 +1648,17 @@ function carriesSentinel(value, sentinel) {
|
|
|
1531
1648
|
*/
|
|
1532
1649
|
export function capturePromptContract(config) {
|
|
1533
1650
|
const rows = [];
|
|
1651
|
+
const initial = initialMachineContext(config);
|
|
1534
1652
|
for (const binding of enumerateCaptainBindings(config)) {
|
|
1535
1653
|
const { state, inputFn } = binding;
|
|
1536
1654
|
if (typeof inputFn !== 'function')
|
|
1537
1655
|
continue;
|
|
1538
|
-
const reads = probeContextReads(inputFn);
|
|
1656
|
+
const reads = probeContextReads(inputFn, initial);
|
|
1539
1657
|
const wires = {};
|
|
1540
1658
|
try {
|
|
1541
|
-
const input = inputFn({
|
|
1659
|
+
const input = inputFn({
|
|
1660
|
+
context: ordinaryTurnContext(reads, initial, true),
|
|
1661
|
+
});
|
|
1542
1662
|
if (typeof input === 'object' && input !== null) {
|
|
1543
1663
|
for (const [key, value] of Object.entries(input)) {
|
|
1544
1664
|
const carried = reads.filter((field) => carriesSentinel(value, sentinelFor(field)));
|
|
@@ -1570,6 +1690,8 @@ export function capturePromptContract(config) {
|
|
|
1570
1690
|
*/
|
|
1571
1691
|
export function deriveSubstitutions(config, compose, actor) {
|
|
1572
1692
|
const out = {};
|
|
1693
|
+
const roles = declaredLocalRoles(config);
|
|
1694
|
+
const initial = initialMachineContext(config);
|
|
1573
1695
|
for (const binding of enumerateCaptainBindings(config)) {
|
|
1574
1696
|
const { state, inputFn } = binding;
|
|
1575
1697
|
if (actor !== undefined && state.actor !== actor)
|
|
@@ -1577,8 +1699,9 @@ export function deriveSubstitutions(config, compose, actor) {
|
|
|
1577
1699
|
if (typeof inputFn !== 'function')
|
|
1578
1700
|
continue;
|
|
1579
1701
|
try {
|
|
1580
|
-
const reads = probeContextReads(inputFn);
|
|
1581
|
-
const
|
|
1702
|
+
const reads = probeContextReads(inputFn, initial);
|
|
1703
|
+
const probed = withPlaceholderValues(state, inputFn({ context: ordinaryTurnContext(reads, initial) }));
|
|
1704
|
+
const composed = composeForState(compose, state, probed.input, roles);
|
|
1582
1705
|
if (typeof composed !== 'string') {
|
|
1583
1706
|
out[state.stateId] = [];
|
|
1584
1707
|
continue;
|
|
@@ -1589,7 +1712,7 @@ export function deriveSubstitutions(config, compose, actor) {
|
|
|
1589
1712
|
// hide valid evidence, while merely deleting a token still cannot
|
|
1590
1713
|
// masquerade as substitution.
|
|
1591
1714
|
const evidenced = new Set();
|
|
1592
|
-
const promptReads = promptSentinelFields(state, reads);
|
|
1715
|
+
const promptReads = promptSentinelFields(state, reads, probed.fields, roles);
|
|
1593
1716
|
for (const line of state.prompt.split('\n')) {
|
|
1594
1717
|
for (const token of matchPromptBody(line, composed, promptReads)
|
|
1595
1718
|
?.substitutions ?? []) {
|
|
@@ -1615,6 +1738,11 @@ export function deriveSubstitutions(config, compose, actor) {
|
|
|
1615
1738
|
export function checkPromptComposition(opts) {
|
|
1616
1739
|
const findings = [];
|
|
1617
1740
|
const controller = isControllerMachine(opts.config);
|
|
1741
|
+
const roles = declaredLocalRoles(opts.config);
|
|
1742
|
+
const initial = initialMachineContext(opts.config);
|
|
1743
|
+
if (initial === undefined) {
|
|
1744
|
+
opts.diagnostics?.push('the machine has no resolvable initial context; ordinary-turn input is synthesized from context sentinels alone');
|
|
1745
|
+
}
|
|
1618
1746
|
const schemaResolution = resolveArtifactSchemaForVerification({
|
|
1619
1747
|
config: opts.config,
|
|
1620
1748
|
...(opts.artifactSchema === undefined
|
|
@@ -1632,12 +1760,14 @@ export function checkPromptComposition(opts) {
|
|
|
1632
1760
|
continue;
|
|
1633
1761
|
if (typeof inputFn !== 'function')
|
|
1634
1762
|
continue;
|
|
1635
|
-
const reads = probeContextReads(inputFn);
|
|
1636
|
-
const promptReads = promptSentinelFields(state, reads);
|
|
1763
|
+
const reads = probeContextReads(inputFn, initial);
|
|
1637
1764
|
const substituted = substitutions[state.stateId] ?? [];
|
|
1638
1765
|
let ordinary;
|
|
1766
|
+
let promptReads;
|
|
1639
1767
|
try {
|
|
1640
|
-
|
|
1768
|
+
const probed = withPlaceholderValues(state, inputFn({ context: ordinaryTurnContext(reads, initial) }));
|
|
1769
|
+
promptReads = promptSentinelFields(state, reads, probed.fields, roles);
|
|
1770
|
+
ordinary = composeForState(opts.compose, state, probed.input, roles);
|
|
1641
1771
|
if (typeof ordinary !== 'string') {
|
|
1642
1772
|
throw new Error(`${composerName} returned a non-string value`);
|
|
1643
1773
|
}
|
|
@@ -1692,9 +1822,9 @@ export function checkPromptComposition(opts) {
|
|
|
1692
1822
|
let continuation;
|
|
1693
1823
|
let input;
|
|
1694
1824
|
try {
|
|
1695
|
-
|
|
1825
|
+
const probed = withPlaceholderValues(state, inputFn({
|
|
1696
1826
|
context: {
|
|
1697
|
-
...
|
|
1827
|
+
...ordinaryTurnContext(reads, initial),
|
|
1698
1828
|
pendingBossQuestion,
|
|
1699
1829
|
bossReply: reply,
|
|
1700
1830
|
pendingBossQuestions: {
|
|
@@ -1702,8 +1832,10 @@ export function checkPromptComposition(opts) {
|
|
|
1702
1832
|
},
|
|
1703
1833
|
bossReplies: { [state.stateId]: reply },
|
|
1704
1834
|
},
|
|
1705
|
-
});
|
|
1706
|
-
|
|
1835
|
+
}));
|
|
1836
|
+
input = probed.input;
|
|
1837
|
+
promptReads = promptSentinelFields(state, reads, probed.fields, roles);
|
|
1838
|
+
continuation = composeForState(opts.compose, state, input, roles);
|
|
1707
1839
|
if (typeof continuation !== 'string') {
|
|
1708
1840
|
throw new Error(`${composerName} returned a non-string value`);
|
|
1709
1841
|
}
|
|
@@ -1746,12 +1878,13 @@ export function checkPromptComposition(opts) {
|
|
|
1746
1878
|
}
|
|
1747
1879
|
return findings;
|
|
1748
1880
|
}
|
|
1749
|
-
function promptSentinelFields(state, reads) {
|
|
1881
|
+
function promptSentinelFields(state, reads, placeholderFields, declaredRoles) {
|
|
1882
|
+
const fields = [...reads, ...placeholderFields];
|
|
1750
1883
|
return state.role === undefined
|
|
1751
|
-
?
|
|
1752
|
-
: [...
|
|
1884
|
+
? fields
|
|
1885
|
+
: [...fields, ...declaredRoles.map((role) => `promptIdentity:${role}`)];
|
|
1753
1886
|
}
|
|
1754
|
-
function composeForState(compose, state, input) {
|
|
1887
|
+
function composeForState(compose, state, input, declaredRoles) {
|
|
1755
1888
|
// Schema-1 composers and the shared default composer use their second
|
|
1756
1889
|
// positional argument as a placeholder-field map. A callable proxy with a
|
|
1757
1890
|
// property-clean view is therefore both an invocation-scoped schema-3 lookup
|
|
@@ -1761,8 +1894,11 @@ function composeForState(compose, state, input) {
|
|
|
1761
1894
|
if (state.role === undefined) {
|
|
1762
1895
|
throw new Error(`prompt identity lookup used role ${JSON.stringify(roleId)} for a direct-Captain or historical state`);
|
|
1763
1896
|
}
|
|
1764
|
-
|
|
1765
|
-
|
|
1897
|
+
// A prompt may name any declared role's identity — a Coder prompt reaches
|
|
1898
|
+
// the Reviewer's `<reviewer-llm>` — so only an undeclared role is drift
|
|
1899
|
+
// (link.md, "Player prompt composition").
|
|
1900
|
+
if (!declaredRoles.includes(roleId)) {
|
|
1901
|
+
throw new Error(`prompt identity lookup used undeclared role ${JSON.stringify(roleId)}; the artifact declares ${JSON.stringify(declaredRoles)}`);
|
|
1766
1902
|
}
|
|
1767
1903
|
return sentinelFor(`promptIdentity:${roleId}`);
|
|
1768
1904
|
};
|
|
@@ -2316,6 +2452,135 @@ export function resolveArtifactSchemaForVerification(opts) {
|
|
|
2316
2452
|
}
|
|
2317
2453
|
: { findings: [] };
|
|
2318
2454
|
}
|
|
2455
|
+
/** The `_internal` composer export serving one state actor (verification-5). */
|
|
2456
|
+
const COMPOSER_EXPORTS = {
|
|
2457
|
+
captain: 'composeCaptainPrompt',
|
|
2458
|
+
player: 'composePlayerPrompt',
|
|
2459
|
+
};
|
|
2460
|
+
/**
|
|
2461
|
+
* Runs the deterministic prompt-contract checks over one live linked module
|
|
2462
|
+
* beside its FSM (verification-5, verification-27): the module imports through
|
|
2463
|
+
* the ephemeral TypeScript FSM edge, its schema evidence reconciles, and every
|
|
2464
|
+
* actor the machine invokes has a composer that honors the link contract.
|
|
2465
|
+
*
|
|
2466
|
+
* Shared by the emitted suite's generator, which pins the derived substitutions
|
|
2467
|
+
* and degrades each observation to a diagnostic, and by the link-fidelity gate,
|
|
2468
|
+
* which relays the same observations as findings — so the gate adds no check of
|
|
2469
|
+
* its own (DR-030).
|
|
2470
|
+
*/
|
|
2471
|
+
async function inspectLinkedPromptContract(opts) {
|
|
2472
|
+
const { config, evidence } = opts;
|
|
2473
|
+
const withoutModule = (extra) => {
|
|
2474
|
+
const resolution = resolveArtifactSchemaForVerification({
|
|
2475
|
+
config,
|
|
2476
|
+
...evidence,
|
|
2477
|
+
});
|
|
2478
|
+
return {
|
|
2479
|
+
...(resolution.artifactSchema === undefined
|
|
2480
|
+
? {}
|
|
2481
|
+
: { artifactSchema: resolution.artifactSchema }),
|
|
2482
|
+
schemaFindings: resolution.findings,
|
|
2483
|
+
present: true,
|
|
2484
|
+
composers: [],
|
|
2485
|
+
...extra,
|
|
2486
|
+
};
|
|
2487
|
+
};
|
|
2488
|
+
if (!existsSync(opts.linkedPath))
|
|
2489
|
+
return withoutModule({ present: false });
|
|
2490
|
+
let linked;
|
|
2491
|
+
try {
|
|
2492
|
+
linked = (await loadLinkedModuleForVerification({
|
|
2493
|
+
linkedPath: opts.linkedPath,
|
|
2494
|
+
fsmPath: opts.fsmPath,
|
|
2495
|
+
}));
|
|
2496
|
+
}
|
|
2497
|
+
catch (error) {
|
|
2498
|
+
return withoutModule({ loadError: messageOf(error) });
|
|
2499
|
+
}
|
|
2500
|
+
const resolution = resolveArtifactSchemaForVerification({
|
|
2501
|
+
config,
|
|
2502
|
+
linked,
|
|
2503
|
+
...evidence,
|
|
2504
|
+
});
|
|
2505
|
+
const artifactSchema = resolution.artifactSchema;
|
|
2506
|
+
const actors = new Set(enumerateCaptainStates(config).map(({ actor }) => actor));
|
|
2507
|
+
const composers = [];
|
|
2508
|
+
for (const actor of ['captain', 'player']) {
|
|
2509
|
+
if (!actors.has(actor))
|
|
2510
|
+
continue;
|
|
2511
|
+
const exportName = COMPOSER_EXPORTS[actor];
|
|
2512
|
+
const compose = linked._internal?.[exportName];
|
|
2513
|
+
if (typeof compose !== 'function') {
|
|
2514
|
+
composers.push({ actor, exportName, findings: [], diagnostics: [] });
|
|
2515
|
+
continue;
|
|
2516
|
+
}
|
|
2517
|
+
const typedCompose = compose;
|
|
2518
|
+
const diagnostics = [];
|
|
2519
|
+
composers.push({
|
|
2520
|
+
actor,
|
|
2521
|
+
exportName,
|
|
2522
|
+
substitutions: deriveSubstitutions(config, typedCompose, actor),
|
|
2523
|
+
findings: checkPromptComposition({
|
|
2524
|
+
config,
|
|
2525
|
+
compose: typedCompose,
|
|
2526
|
+
actor,
|
|
2527
|
+
...(artifactSchema === undefined ? {} : { artifactSchema }),
|
|
2528
|
+
diagnostics,
|
|
2529
|
+
}),
|
|
2530
|
+
diagnostics,
|
|
2531
|
+
});
|
|
2532
|
+
}
|
|
2533
|
+
return {
|
|
2534
|
+
...(artifactSchema === undefined ? {} : { artifactSchema }),
|
|
2535
|
+
schemaFindings: resolution.findings,
|
|
2536
|
+
present: true,
|
|
2537
|
+
composers,
|
|
2538
|
+
};
|
|
2539
|
+
}
|
|
2540
|
+
/**
|
|
2541
|
+
* The deterministic link-fidelity checks over one live linked `playbook` module
|
|
2542
|
+
* beside its FSM (verification-27): exactly the checks the emitted
|
|
2543
|
+
* prompt-contract suite asserts, decided from those two paths alone and
|
|
2544
|
+
* returning one finding per violation.
|
|
2545
|
+
*
|
|
2546
|
+
* The gate that runs this before any Reviewer call must never throw into the
|
|
2547
|
+
* executor it guards (DR-030, phase-execution-53), so a module that cannot be
|
|
2548
|
+
* imported yields its import diagnostic as a finding. An absent module or FSM
|
|
2549
|
+
* yields none: an artifact the performing call has not written is the generic
|
|
2550
|
+
* target check's business (phase-execution-4). An FSM that cannot be imported
|
|
2551
|
+
* yields none either — the suite's checks derive from that machine, so they
|
|
2552
|
+
* degrade exactly as emission degrades its prompt-contract test to a
|
|
2553
|
+
* diagnostic (verification-5).
|
|
2554
|
+
*/
|
|
2555
|
+
export async function checkLinkedModuleContract(opts) {
|
|
2556
|
+
if (!existsSync(opts.linkedPath) || !existsSync(opts.fsmPath))
|
|
2557
|
+
return [];
|
|
2558
|
+
let config;
|
|
2559
|
+
try {
|
|
2560
|
+
config = findMachineConfig(await loadFsmModule(opts.fsmPath));
|
|
2561
|
+
}
|
|
2562
|
+
catch {
|
|
2563
|
+
return [];
|
|
2564
|
+
}
|
|
2565
|
+
const contract = await inspectLinkedPromptContract({
|
|
2566
|
+
config,
|
|
2567
|
+
linkedPath: opts.linkedPath,
|
|
2568
|
+
fsmPath: opts.fsmPath,
|
|
2569
|
+
evidence: {},
|
|
2570
|
+
});
|
|
2571
|
+
const findings = [...contract.schemaFindings];
|
|
2572
|
+
if (contract.loadError !== undefined) {
|
|
2573
|
+
findings.push(`linked module could not be imported: ${contract.loadError}`);
|
|
2574
|
+
return findings;
|
|
2575
|
+
}
|
|
2576
|
+
// A module exposing no matching composer degrades exactly as emission does
|
|
2577
|
+
// (verification-5): the suite asserts nothing about it, so neither does the
|
|
2578
|
+
// gate that reuses the suite's checks.
|
|
2579
|
+
for (const composer of contract.composers) {
|
|
2580
|
+
findings.push(...composer.findings);
|
|
2581
|
+
}
|
|
2582
|
+
return findings;
|
|
2583
|
+
}
|
|
2319
2584
|
/**
|
|
2320
2585
|
* Emits the prompt-contract test beside a compiled `playbook` artifact
|
|
2321
2586
|
* (verification-5): derives and pins the per-state contract from the physical
|
|
@@ -2344,66 +2609,39 @@ export async function emitPromptContractTest(opts) {
|
|
|
2344
2609
|
? {}
|
|
2345
2610
|
: { artifactSchema: opts.artifactSchema }),
|
|
2346
2611
|
};
|
|
2347
|
-
|
|
2612
|
+
const contract = await inspectLinkedPromptContract({
|
|
2348
2613
|
config,
|
|
2349
|
-
|
|
2614
|
+
linkedPath: join(opts.artifactDir, `${opts.basename}.playbook.ts`),
|
|
2615
|
+
fsmPath,
|
|
2616
|
+
evidence,
|
|
2350
2617
|
});
|
|
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`);
|
|
2618
|
+
const artifactSchema = contract.artifactSchema;
|
|
2619
|
+
if (contract.loadError !== undefined) {
|
|
2620
|
+
diagnostics.push(`prompt contract: linked module could not be imported (${contract.loadError}); composition checks not emitted`);
|
|
2621
|
+
}
|
|
2622
|
+
const substitutions = {};
|
|
2623
|
+
for (const inspected of contract.composers) {
|
|
2624
|
+
if (inspected.substitutions === undefined) {
|
|
2625
|
+
diagnostics.push(`prompt contract: linked module exposes no _internal.${inspected.exportName}; ${inspected.actor} composition checks not emitted`);
|
|
2626
|
+
continue;
|
|
2397
2627
|
}
|
|
2628
|
+
substitutions[inspected.actor] = inspected.substitutions;
|
|
2629
|
+
diagnostics.push(...inspected.diagnostics.map((diagnostic) => `prompt contract: ${diagnostic}`), ...inspected.findings.map((finding) => `prompt contract: ${finding}`));
|
|
2398
2630
|
}
|
|
2399
|
-
|
|
2631
|
+
const composer = substitutions.captain !== undefined || substitutions.player !== undefined
|
|
2632
|
+
? {
|
|
2633
|
+
playbookModule: `./${opts.basename}.playbook.js`,
|
|
2634
|
+
...substitutions,
|
|
2635
|
+
}
|
|
2636
|
+
: undefined;
|
|
2637
|
+
diagnostics.unshift(...contract.schemaFindings.map((finding) => `prompt contract: ${finding}`));
|
|
2400
2638
|
const content = generatePromptContractTest({
|
|
2401
2639
|
basename: opts.basename,
|
|
2402
2640
|
fsmModule: `./${opts.basename}.fsm.js`,
|
|
2403
2641
|
verifyModule: opts.verifyModule ?? VERIFY_MODULE,
|
|
2404
2642
|
rows,
|
|
2405
2643
|
...(artifactSchema === undefined ? {} : { artifactSchema }),
|
|
2406
|
-
schemaFindings:
|
|
2644
|
+
schemaFindings: contract.schemaFindings,
|
|
2407
2645
|
composer,
|
|
2408
2646
|
});
|
|
2409
2647
|
await mkdir(opts.artifactDir, { recursive: true });
|