@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.
Files changed (41) hide show
  1. package/dist/cligent-agent.d.ts.map +1 -1
  2. package/dist/cligent-agent.js +43 -3
  3. package/dist/cligent-agent.js.map +1 -1
  4. package/dist/compiled-executor.d.ts.map +1 -1
  5. package/dist/compiled-executor.js +63 -9
  6. package/dist/compiled-executor.js.map +1 -1
  7. package/dist/entry-module.d.ts +0 -8
  8. package/dist/entry-module.d.ts.map +1 -1
  9. package/dist/entry-module.js +75 -77
  10. package/dist/entry-module.js.map +1 -1
  11. package/dist/execution.d.ts +19 -0
  12. package/dist/execution.d.ts.map +1 -1
  13. package/dist/execution.js.map +1 -1
  14. package/dist/interpreter.d.ts +18 -2
  15. package/dist/interpreter.d.ts.map +1 -1
  16. package/dist/interpreter.js +6 -0
  17. package/dist/interpreter.js.map +1 -1
  18. package/dist/link.d.ts +7 -0
  19. package/dist/link.d.ts.map +1 -1
  20. package/dist/link.js +4 -2
  21. package/dist/link.js.map +1 -1
  22. package/dist/playbook-ports.d.ts +11 -1
  23. package/dist/playbook-ports.d.ts.map +1 -1
  24. package/dist/playbook-ports.js +8 -1
  25. package/dist/playbook-ports.js.map +1 -1
  26. package/dist/reviewing-agent.d.ts +9 -2
  27. package/dist/reviewing-agent.d.ts.map +1 -1
  28. package/dist/reviewing-agent.js +232 -38
  29. package/dist/reviewing-agent.js.map +1 -1
  30. package/dist/runner.d.ts.map +1 -1
  31. package/dist/runner.js +171 -32
  32. package/dist/runner.js.map +1 -1
  33. package/dist/verify-source.d.ts +61 -0
  34. package/dist/verify-source.d.ts.map +1 -0
  35. package/dist/verify-source.js +280 -0
  36. package/dist/verify-source.js.map +1 -0
  37. package/dist/verify.d.ts +26 -1
  38. package/dist/verify.d.ts.map +1 -1
  39. package/dist/verify.js +313 -75
  40. package/dist/verify.js.map +1 -1
  41. 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(_target, prop) {
1493
+ const context = new Proxy({ ...initial }, {
1494
+ get(target, prop) {
1489
1495
  if (typeof prop === 'string')
1490
1496
  reads.add(prop);
1491
- return undefined;
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({ context: sentinelContext(reads) });
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 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);
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
- 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);
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
- input = inputFn({
1825
+ const probed = withPlaceholderValues(state, inputFn({
1696
1826
  context: {
1697
- ...ordinaryContext(reads),
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
- 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);
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
- ? [...reads]
1752
- : [...reads, `promptIdentity:${state.role}`];
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
- if (roleId !== state.role) {
1765
- throw new Error(`prompt identity lookup used role ${JSON.stringify(roleId)} instead of canonical local role ${JSON.stringify(state.role)}`);
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
- let schemaResolution = resolveArtifactSchemaForVerification({
2612
+ const contract = await inspectLinkedPromptContract({
2348
2613
  config,
2349
- ...evidence,
2614
+ linkedPath: join(opts.artifactDir, `${opts.basename}.playbook.ts`),
2615
+ fsmPath,
2616
+ evidence,
2350
2617
  });
2351
- let artifactSchema = schemaResolution.artifactSchema;
2352
- let composer;
2353
- const linkedPath = join(opts.artifactDir, `${opts.basename}.playbook.ts`);
2354
- if (existsSync(linkedPath)) {
2355
- try {
2356
- const linked = (await loadLinkedModuleForVerification({
2357
- linkedPath,
2358
- fsmPath,
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
- 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}`));
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: schemaResolution.findings,
2644
+ schemaFindings: contract.schemaFindings,
2407
2645
  composer,
2408
2646
  });
2409
2647
  await mkdir(opts.artifactDir, { recursive: true });