@sublang/slc 0.6.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.
Files changed (79) hide show
  1. package/README.md +7 -2
  2. package/dist/app.d.ts.map +1 -1
  3. package/dist/app.js +15 -2
  4. package/dist/app.js.map +1 -1
  5. package/dist/cligent-agent.d.ts +8 -1
  6. package/dist/cligent-agent.d.ts.map +1 -1
  7. package/dist/cligent-agent.js +45 -4
  8. package/dist/cligent-agent.js.map +1 -1
  9. package/dist/compiled-execution.d.ts +44 -0
  10. package/dist/compiled-execution.d.ts.map +1 -0
  11. package/dist/compiled-execution.js +184 -0
  12. package/dist/compiled-execution.js.map +1 -0
  13. package/dist/compiled-executor.d.ts +9 -7
  14. package/dist/compiled-executor.d.ts.map +1 -1
  15. package/dist/compiled-executor.js +143 -44
  16. package/dist/compiled-executor.js.map +1 -1
  17. package/dist/config-file.d.ts +3 -0
  18. package/dist/config-file.d.ts.map +1 -1
  19. package/dist/config-file.js +22 -4
  20. package/dist/config-file.js.map +1 -1
  21. package/dist/config.d.ts +66 -15
  22. package/dist/config.d.ts.map +1 -1
  23. package/dist/config.js +115 -49
  24. package/dist/config.js.map +1 -1
  25. package/dist/entry-module.d.ts +0 -8
  26. package/dist/entry-module.d.ts.map +1 -1
  27. package/dist/entry-module.js +75 -77
  28. package/dist/entry-module.js.map +1 -1
  29. package/dist/execution.d.ts +19 -0
  30. package/dist/execution.d.ts.map +1 -1
  31. package/dist/execution.js.map +1 -1
  32. package/dist/interpreter.d.ts +18 -2
  33. package/dist/interpreter.d.ts.map +1 -1
  34. package/dist/interpreter.js +6 -0
  35. package/dist/interpreter.js.map +1 -1
  36. package/dist/link.d.ts +7 -0
  37. package/dist/link.d.ts.map +1 -1
  38. package/dist/link.js +4 -2
  39. package/dist/link.js.map +1 -1
  40. package/dist/phase-runner.d.ts +9 -2
  41. package/dist/phase-runner.d.ts.map +1 -1
  42. package/dist/phase-runner.js +3 -1
  43. package/dist/phase-runner.js.map +1 -1
  44. package/dist/pin-generate.d.ts.map +1 -1
  45. package/dist/pin-generate.js +10 -2
  46. package/dist/pin-generate.js.map +1 -1
  47. package/dist/playbook-contract.d.ts +18 -22
  48. package/dist/playbook-contract.d.ts.map +1 -1
  49. package/dist/playbook-contract.js.map +1 -1
  50. package/dist/playbook-ports.d.ts +11 -1
  51. package/dist/playbook-ports.d.ts.map +1 -1
  52. package/dist/playbook-ports.js +8 -1
  53. package/dist/playbook-ports.js.map +1 -1
  54. package/dist/reviewing-agent.d.ts +9 -2
  55. package/dist/reviewing-agent.d.ts.map +1 -1
  56. package/dist/reviewing-agent.js +232 -38
  57. package/dist/reviewing-agent.js.map +1 -1
  58. package/dist/runner.d.ts +1 -1
  59. package/dist/runner.d.ts.map +1 -1
  60. package/dist/runner.js +200 -39
  61. package/dist/runner.js.map +1 -1
  62. package/dist/runtime-contract.d.ts +17 -0
  63. package/dist/runtime-contract.d.ts.map +1 -0
  64. package/dist/runtime-contract.js +114 -0
  65. package/dist/runtime-contract.js.map +1 -0
  66. package/dist/slc.config.template.yaml +7 -2
  67. package/dist/verify-source.d.ts +61 -0
  68. package/dist/verify-source.d.ts.map +1 -0
  69. package/dist/verify-source.js +280 -0
  70. package/dist/verify-source.js.map +1 -0
  71. package/dist/verify.d.ts +70 -3
  72. package/dist/verify.d.ts.map +1 -1
  73. package/dist/verify.js +375 -86
  74. package/dist/verify.js.map +1 -1
  75. package/package.json +3 -3
  76. package/dist/host-capabilities.d.ts +0 -115
  77. package/dist/host-capabilities.d.ts.map +0 -1
  78. package/dist/host-capabilities.js +0 -507
  79. package/dist/host-capabilities.js.map +0 -1
package/dist/verify.js CHANGED
@@ -32,7 +32,41 @@ import { hashFile } from './hash.js';
32
32
  */
33
33
  export const NEEDS_BOSS_REPLY = 'needsBossReply';
34
34
  export const BOSS_QUESTION_MARKER = 'Output shall include `question:';
35
- /** Artifact schema selected only by a complete reviewed Playbook provenance. */
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
+ */
36
70
  export function artifactSchemaForPlaybookProvenance(provenance) {
37
71
  switch (provenance) {
38
72
  case '@sublang/playbook@0.10.0':
@@ -522,13 +556,14 @@ function structuredStateIdentityFindings(nodes) {
522
556
  }
523
557
  function enumerateCaptainBindings(config) {
524
558
  const out = [];
559
+ const initial = initialMachineContext(config) ?? {};
525
560
  for (const node of walkStateNodes(config)) {
526
561
  for (const invoke of normalizeInvokes(node.state.invoke)) {
527
562
  const source = invokeSource(invoke.src);
528
563
  const explicitlyWorkActor = source === 'captain' || source === 'player';
529
564
  if (!explicitlyWorkActor && invoke.src !== undefined)
530
565
  continue;
531
- const inspected = invocationInput(invoke);
566
+ const inspected = invocationInput(invoke, initial);
532
567
  if ('error' in inspected) {
533
568
  if (explicitlyWorkActor) {
534
569
  const actor = source === 'player' ? 'player' : 'captain';
@@ -654,11 +689,12 @@ export function enumerateCaptainStates(config) {
654
689
  }
655
690
  function enumeratePlaybookBindings(config) {
656
691
  const out = [];
692
+ const initial = initialMachineContext(config) ?? {};
657
693
  for (const node of walkStateNodes(config)) {
658
694
  for (const invoke of normalizeInvokes(node.state.invoke)) {
659
695
  if (invokeSource(invoke.src) !== 'playbook')
660
696
  continue;
661
- const inspected = invocationInput(invoke);
697
+ const inspected = invocationInput(invoke, initial);
662
698
  if ('error' in inspected) {
663
699
  out.push({
664
700
  state: malformedPlaybookState(publicStateId(node, undefined), `invoke.input threw during introspection: ${inspected.error}`, node),
@@ -696,6 +732,7 @@ function enumeratePlaybookBindings(config) {
696
732
  const playbookIdSentinel = sentinelFor(fields.playbookIdContext);
697
733
  const textSentinel = sentinelFor(fields.textContext);
698
734
  const wired = invocationInput(invoke, {
735
+ ...initial,
699
736
  [fields.playbookIdContext]: playbookIdSentinel,
700
737
  [fields.textContext]: textSentinel,
701
738
  });
@@ -765,6 +802,7 @@ export function enumeratePlaybookStates(config) {
765
802
  */
766
803
  export function enumerateScriptStates(config) {
767
804
  const out = [];
805
+ const initial = initialMachineContext(config) ?? {};
768
806
  for (const node of walkStateNodes(config)) {
769
807
  for (const invoke of normalizeInvokes(node.state.invoke)) {
770
808
  if (invokeSource(invoke.src) !== 'script')
@@ -777,7 +815,7 @@ export function enumerateScriptStates(config) {
777
815
  ...nestedStatePath(node),
778
816
  bindingFindings: [finding],
779
817
  });
780
- const inspected = invocationInput(invoke);
818
+ const inspected = invocationInput(invoke, initial);
781
819
  if ('error' in inspected) {
782
820
  out.push(malformed(`invoke.input threw during introspection: ${inspected.error}`));
783
821
  continue;
@@ -1448,13 +1486,15 @@ const sentinelFor = (field) => `«${field}»`;
1448
1486
  * Traces which context fields an `invoke.input` thunk reads, via a recording
1449
1487
  * proxy context; reads collected up to a throw are kept.
1450
1488
  */
1451
- 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 = {}) {
1452
1492
  const reads = new Set();
1453
- const context = new Proxy({}, {
1454
- get(_target, prop) {
1493
+ const context = new Proxy({ ...initial }, {
1494
+ get(target, prop) {
1455
1495
  if (typeof prop === 'string')
1456
1496
  reads.add(prop);
1457
- return undefined;
1497
+ return Reflect.get(target, prop);
1458
1498
  },
1459
1499
  has() {
1460
1500
  return true;
@@ -1482,6 +1522,117 @@ const BOSS_CONTEXT_FIELDS = [
1482
1522
  function ordinaryContext(reads) {
1483
1523
  return sentinelContext(reads.filter((field) => !BOSS_CONTEXT_FIELDS.includes(field)));
1484
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
+ }
1485
1636
  function carriesSentinel(value, sentinel) {
1486
1637
  try {
1487
1638
  return (JSON.stringify(value) ?? '').includes(sentinel);
@@ -1497,14 +1648,17 @@ function carriesSentinel(value, sentinel) {
1497
1648
  */
1498
1649
  export function capturePromptContract(config) {
1499
1650
  const rows = [];
1651
+ const initial = initialMachineContext(config);
1500
1652
  for (const binding of enumerateCaptainBindings(config)) {
1501
1653
  const { state, inputFn } = binding;
1502
1654
  if (typeof inputFn !== 'function')
1503
1655
  continue;
1504
- const reads = probeContextReads(inputFn);
1656
+ const reads = probeContextReads(inputFn, initial);
1505
1657
  const wires = {};
1506
1658
  try {
1507
- const input = inputFn({ context: sentinelContext(reads) });
1659
+ const input = inputFn({
1660
+ context: ordinaryTurnContext(reads, initial, true),
1661
+ });
1508
1662
  if (typeof input === 'object' && input !== null) {
1509
1663
  for (const [key, value] of Object.entries(input)) {
1510
1664
  const carried = reads.filter((field) => carriesSentinel(value, sentinelFor(field)));
@@ -1536,6 +1690,8 @@ export function capturePromptContract(config) {
1536
1690
  */
1537
1691
  export function deriveSubstitutions(config, compose, actor) {
1538
1692
  const out = {};
1693
+ const roles = declaredLocalRoles(config);
1694
+ const initial = initialMachineContext(config);
1539
1695
  for (const binding of enumerateCaptainBindings(config)) {
1540
1696
  const { state, inputFn } = binding;
1541
1697
  if (actor !== undefined && state.actor !== actor)
@@ -1543,8 +1699,9 @@ export function deriveSubstitutions(config, compose, actor) {
1543
1699
  if (typeof inputFn !== 'function')
1544
1700
  continue;
1545
1701
  try {
1546
- const reads = probeContextReads(inputFn);
1547
- const composed = composeForState(compose, state, inputFn({ context: ordinaryContext(reads) }));
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);
1548
1705
  if (typeof composed !== 'string') {
1549
1706
  out[state.stateId] = [];
1550
1707
  continue;
@@ -1555,7 +1712,7 @@ export function deriveSubstitutions(config, compose, actor) {
1555
1712
  // hide valid evidence, while merely deleting a token still cannot
1556
1713
  // masquerade as substitution.
1557
1714
  const evidenced = new Set();
1558
- const promptReads = promptSentinelFields(state, reads);
1715
+ const promptReads = promptSentinelFields(state, reads, probed.fields, roles);
1559
1716
  for (const line of state.prompt.split('\n')) {
1560
1717
  for (const token of matchPromptBody(line, composed, promptReads)
1561
1718
  ?.substitutions ?? []) {
@@ -1581,6 +1738,11 @@ export function deriveSubstitutions(config, compose, actor) {
1581
1738
  export function checkPromptComposition(opts) {
1582
1739
  const findings = [];
1583
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
+ }
1584
1746
  const schemaResolution = resolveArtifactSchemaForVerification({
1585
1747
  config: opts.config,
1586
1748
  ...(opts.artifactSchema === undefined
@@ -1598,12 +1760,14 @@ export function checkPromptComposition(opts) {
1598
1760
  continue;
1599
1761
  if (typeof inputFn !== 'function')
1600
1762
  continue;
1601
- const reads = probeContextReads(inputFn);
1602
- const promptReads = promptSentinelFields(state, reads);
1763
+ const reads = probeContextReads(inputFn, initial);
1603
1764
  const substituted = substitutions[state.stateId] ?? [];
1604
1765
  let ordinary;
1766
+ let promptReads;
1605
1767
  try {
1606
- ordinary = composeForState(opts.compose, state, inputFn({ context: ordinaryContext(reads) }));
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);
1607
1771
  if (typeof ordinary !== 'string') {
1608
1772
  throw new Error(`${composerName} returned a non-string value`);
1609
1773
  }
@@ -1658,9 +1822,9 @@ export function checkPromptComposition(opts) {
1658
1822
  let continuation;
1659
1823
  let input;
1660
1824
  try {
1661
- input = inputFn({
1825
+ const probed = withPlaceholderValues(state, inputFn({
1662
1826
  context: {
1663
- ...ordinaryContext(reads),
1827
+ ...ordinaryTurnContext(reads, initial),
1664
1828
  pendingBossQuestion,
1665
1829
  bossReply: reply,
1666
1830
  pendingBossQuestions: {
@@ -1668,8 +1832,10 @@ export function checkPromptComposition(opts) {
1668
1832
  },
1669
1833
  bossReplies: { [state.stateId]: reply },
1670
1834
  },
1671
- });
1672
- continuation = composeForState(opts.compose, state, input);
1835
+ }));
1836
+ input = probed.input;
1837
+ promptReads = promptSentinelFields(state, reads, probed.fields, roles);
1838
+ continuation = composeForState(opts.compose, state, input, roles);
1673
1839
  if (typeof continuation !== 'string') {
1674
1840
  throw new Error(`${composerName} returned a non-string value`);
1675
1841
  }
@@ -1712,12 +1878,13 @@ export function checkPromptComposition(opts) {
1712
1878
  }
1713
1879
  return findings;
1714
1880
  }
1715
- function promptSentinelFields(state, reads) {
1881
+ function promptSentinelFields(state, reads, placeholderFields, declaredRoles) {
1882
+ const fields = [...reads, ...placeholderFields];
1716
1883
  return state.role === undefined
1717
- ? [...reads]
1718
- : [...reads, `promptIdentity:${state.role}`];
1884
+ ? fields
1885
+ : [...fields, ...declaredRoles.map((role) => `promptIdentity:${role}`)];
1719
1886
  }
1720
- function composeForState(compose, state, input) {
1887
+ function composeForState(compose, state, input, declaredRoles) {
1721
1888
  // Schema-1 composers and the shared default composer use their second
1722
1889
  // positional argument as a placeholder-field map. A callable proxy with a
1723
1890
  // property-clean view is therefore both an invocation-scoped schema-3 lookup
@@ -1727,8 +1894,11 @@ function composeForState(compose, state, input) {
1727
1894
  if (state.role === undefined) {
1728
1895
  throw new Error(`prompt identity lookup used role ${JSON.stringify(roleId)} for a direct-Captain or historical state`);
1729
1896
  }
1730
- if (roleId !== state.role) {
1731
- throw new Error(`prompt identity lookup used role ${JSON.stringify(roleId)} instead of canonical local role ${JSON.stringify(state.role)}`);
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)}`);
1732
1902
  }
1733
1903
  return sentinelFor(`promptIdentity:${roleId}`);
1734
1904
  };
@@ -2196,7 +2366,14 @@ function linkedArtifactSchemaSignal(linked) {
2196
2366
  ? { schema: 3, historicalFallback: false, invalidCompatibility: false }
2197
2367
  : { historicalFallback: false, invalidCompatibility: true };
2198
2368
  }
2199
- /** Schema decision shared by generated and standalone artifact verification. */
2369
+ /**
2370
+ * Schema decision shared by generated and standalone artifact verification
2371
+ * (verification-21). Reviewed provenance is evidence through its exact
2372
+ * historical map; any other provenance is evidence only through the engine
2373
+ * declaration read from the link target's installed package — `RUNTIME_ABI`
2374
+ * `1` with artifact schema `3` — and is otherwise reported unsupported
2375
+ * (DR-028).
2376
+ */
2200
2377
  export function resolveArtifactSchemaForVerification(opts) {
2201
2378
  const candidates = [];
2202
2379
  const invalidSignalFindings = [];
@@ -2206,16 +2383,25 @@ export function resolveArtifactSchemaForVerification(opts) {
2206
2383
  schema: opts.artifactSchema,
2207
2384
  });
2208
2385
  }
2209
- const provenanceSchema = artifactSchemaForPlaybookProvenance(opts.provenance);
2210
- if (opts.provenance !== undefined && provenanceSchema === undefined) {
2211
- invalidSignalFindings.push(`artifact schema has unsupported link-target provenance ${JSON.stringify(opts.provenance)}`);
2212
- }
2386
+ const provenance = opts.provenance ?? opts.runtimeDeclaration?.provenance;
2387
+ const provenanceSchema = artifactSchemaForPlaybookProvenance(provenance);
2213
2388
  if (provenanceSchema !== undefined) {
2214
2389
  candidates.push({
2215
2390
  source: 'reviewed link-target provenance',
2216
2391
  schema: provenanceSchema,
2217
2392
  });
2218
2393
  }
2394
+ else if (provenance !== undefined) {
2395
+ if (opts.runtimeDeclaration === undefined) {
2396
+ invalidSignalFindings.push(`artifact schema has unsupported link-target provenance ${JSON.stringify(provenance)}`);
2397
+ }
2398
+ else if (declaresComposedV3(opts.runtimeDeclaration)) {
2399
+ candidates.push({ source: 'declared link-target contract', schema: 3 });
2400
+ }
2401
+ else {
2402
+ invalidSignalFindings.push(`artifact schema has an unsupported link-target contract: ${describeRuntimeDeclaration(opts.runtimeDeclaration)}`);
2403
+ }
2404
+ }
2219
2405
  if (opts.config !== undefined) {
2220
2406
  const configSignals = promptArtifactSchemaSignalsFromConfig(opts.config);
2221
2407
  if (configSignals.schema1)
@@ -2266,6 +2452,135 @@ export function resolveArtifactSchemaForVerification(opts) {
2266
2452
  }
2267
2453
  : { findings: [] };
2268
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
+ }
2269
2584
  /**
2270
2585
  * Emits the prompt-contract test beside a compiled `playbook` artifact
2271
2586
  * (verification-5): derives and pins the per-state contract from the physical
@@ -2285,74 +2600,48 @@ export async function emitPromptContractTest(opts) {
2285
2600
  const fsmPath = join(opts.artifactDir, `${opts.basename}.fsm.ts`);
2286
2601
  const config = findMachineConfig(await loadFsmModule(fsmPath));
2287
2602
  const rows = capturePromptContract(config);
2288
- let schemaResolution = resolveArtifactSchemaForVerification({
2289
- config,
2603
+ const evidence = {
2290
2604
  ...(opts.provenance === undefined ? {} : { provenance: opts.provenance }),
2605
+ ...(opts.runtimeDeclaration === undefined
2606
+ ? {}
2607
+ : { runtimeDeclaration: opts.runtimeDeclaration }),
2291
2608
  ...(opts.artifactSchema === undefined
2292
2609
  ? {}
2293
2610
  : { artifactSchema: opts.artifactSchema }),
2611
+ };
2612
+ const contract = await inspectLinkedPromptContract({
2613
+ config,
2614
+ linkedPath: join(opts.artifactDir, `${opts.basename}.playbook.ts`),
2615
+ fsmPath,
2616
+ evidence,
2294
2617
  });
2295
- let artifactSchema = schemaResolution.artifactSchema;
2296
- let composer;
2297
- const linkedPath = join(opts.artifactDir, `${opts.basename}.playbook.ts`);
2298
- if (existsSync(linkedPath)) {
2299
- try {
2300
- const linked = (await loadLinkedModuleForVerification({
2301
- linkedPath,
2302
- fsmPath,
2303
- }));
2304
- schemaResolution = resolveArtifactSchemaForVerification({
2305
- config,
2306
- linked,
2307
- ...(opts.provenance === undefined
2308
- ? {}
2309
- : { provenance: opts.provenance }),
2310
- ...(opts.artifactSchema === undefined
2311
- ? {}
2312
- : { artifactSchema: opts.artifactSchema }),
2313
- });
2314
- artifactSchema = schemaResolution.artifactSchema;
2315
- const actors = new Set(enumerateCaptainStates(config).map(({ actor }) => actor));
2316
- const substitutions = {};
2317
- for (const actor of ['captain', 'player']) {
2318
- if (!actors.has(actor))
2319
- continue;
2320
- const exportName = actor === 'captain' ? 'composeCaptainPrompt' : 'composePlayerPrompt';
2321
- const compose = linked._internal?.[exportName];
2322
- if (typeof compose !== 'function') {
2323
- diagnostics.push(`prompt contract: linked module exposes no _internal.${exportName}; ${actor} composition checks not emitted`);
2324
- continue;
2325
- }
2326
- const typedCompose = compose;
2327
- substitutions[actor] = deriveSubstitutions(config, typedCompose, actor);
2328
- const findings = checkPromptComposition({
2329
- config,
2330
- compose: typedCompose,
2331
- actor,
2332
- ...(artifactSchema === undefined ? {} : { artifactSchema }),
2333
- });
2334
- diagnostics.push(...findings.map((finding) => `prompt contract: ${finding}`));
2335
- }
2336
- if (substitutions.captain !== undefined ||
2337
- substitutions.player !== undefined) {
2338
- composer = {
2339
- playbookModule: `./${opts.basename}.playbook.js`,
2340
- ...substitutions,
2341
- };
2342
- }
2343
- }
2344
- catch (error) {
2345
- 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;
2346
2627
  }
2628
+ substitutions[inspected.actor] = inspected.substitutions;
2629
+ diagnostics.push(...inspected.diagnostics.map((diagnostic) => `prompt contract: ${diagnostic}`), ...inspected.findings.map((finding) => `prompt contract: ${finding}`));
2347
2630
  }
2348
- diagnostics.unshift(...schemaResolution.findings.map((finding) => `prompt contract: ${finding}`));
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}`));
2349
2638
  const content = generatePromptContractTest({
2350
2639
  basename: opts.basename,
2351
2640
  fsmModule: `./${opts.basename}.fsm.js`,
2352
2641
  verifyModule: opts.verifyModule ?? VERIFY_MODULE,
2353
2642
  rows,
2354
2643
  ...(artifactSchema === undefined ? {} : { artifactSchema }),
2355
- schemaFindings: schemaResolution.findings,
2644
+ schemaFindings: contract.schemaFindings,
2356
2645
  composer,
2357
2646
  });
2358
2647
  await mkdir(opts.artifactDir, { recursive: true });