@sublang/playbook 0.1.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.
@@ -0,0 +1,129 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2026 SubLang International <https://sublang.ai>
3
+
4
+ // FSM introspection helpers — IR-005 Task 2.
5
+ // Static walkers over `codingMachine.config` consumed by the
6
+ // conformance, coverage, and prompt-contract tests landing in
7
+ // Tasks 3–5. Reaching into `machine.config` is internal but stable
8
+ // in xstate v5: it preserves the literal `createMachine` argument,
9
+ // so `invoke.input` is still the original `({ context }) =>
10
+ // CaptainInput` function and `invoke.onDone` is still the per-arm
11
+ // array with `guard` / `target` / `actions` keys.
12
+
13
+ import type {
14
+ CaptainInput,
15
+ CodingContext,
16
+ codingMachine,
17
+ } from './code.fsm.js';
18
+
19
+ export interface CaptainStateInfo {
20
+ readonly stateId: string;
21
+ readonly sourceItem: string;
22
+ readonly getInput: (context: Partial<CodingContext>) => CaptainInput;
23
+ readonly transitions: ReadonlyArray<CaptainTransition>;
24
+ }
25
+
26
+ export interface CaptainTransition {
27
+ // Position in the state's `invoke.onDone` array. Stable per FSM
28
+ // source — the coverage test's fixture table keys
29
+ // `(stateId, index)` to address each arm uniquely, since
30
+ // `(stateId, target)` collides for arms that share a target
31
+ // (e.g., `commitReviewerCleared` and `commitJoint` each route
32
+ // both `committed && afterReview === 'done'` and
33
+ // `noRelevantChanges` to `done`).
34
+ readonly index: number;
35
+ readonly target: string;
36
+ readonly guard: TransitionGuard;
37
+ }
38
+
39
+ export type TransitionGuard = (args: {
40
+ context: CodingContext;
41
+ event: unknown;
42
+ }) => boolean;
43
+
44
+ export interface RootEventTable {
45
+ readonly startCoding: { readonly target: string };
46
+ readonly continueIr: { readonly target: string };
47
+ readonly summarizeIr: { readonly target: string };
48
+ readonly bossInterruptTargets: ReadonlyArray<string>;
49
+ }
50
+
51
+ type RawInvoke = {
52
+ src?: unknown;
53
+ input?: (args: { context: Partial<CodingContext> }) => CaptainInput;
54
+ onDone?: unknown;
55
+ };
56
+
57
+ type RawStateDef = { invoke?: RawInvoke; on?: Record<string, unknown> };
58
+
59
+ type RawArm = { target?: unknown; guard?: TransitionGuard };
60
+
61
+ type RawConfig = {
62
+ states?: Record<string, RawStateDef>;
63
+ on?: Record<string, unknown>;
64
+ };
65
+
66
+ export function enumerateCaptainStates(
67
+ machine: typeof codingMachine,
68
+ ): readonly CaptainStateInfo[] {
69
+ const states = getRawConfig(machine).states ?? {};
70
+ const out: CaptainStateInfo[] = [];
71
+ for (const [stateId, def] of Object.entries(states)) {
72
+ const invoke = def.invoke;
73
+ if (!invoke || invoke.src !== 'captain' || typeof invoke.input !== 'function') {
74
+ continue;
75
+ }
76
+ const inputFn = invoke.input;
77
+ const getInput = (context: Partial<CodingContext>): CaptainInput =>
78
+ inputFn({ context });
79
+ const probe = getInput({});
80
+ const transitions = toArmArray(invoke.onDone).map(
81
+ (arm, index): CaptainTransition => ({
82
+ index,
83
+ target: stripIdPrefix(String(arm.target ?? '')),
84
+ guard: arm.guard ?? alwaysTrue,
85
+ }),
86
+ );
87
+ out.push({
88
+ stateId,
89
+ sourceItem: probe.sourceItem,
90
+ getInput,
91
+ transitions,
92
+ });
93
+ }
94
+ return out;
95
+ }
96
+
97
+ export function enumerateRootEvents(
98
+ machine: typeof codingMachine,
99
+ ): RootEventTable {
100
+ const cfg = getRawConfig(machine);
101
+ const readyOn = (cfg.states?.ready?.on ?? {}) as Record<
102
+ string,
103
+ { target?: string }
104
+ >;
105
+ const rootOn = (cfg.on ?? {}) as Record<string, unknown>;
106
+ return {
107
+ startCoding: { target: stripIdPrefix(String(readyOn.START_CODING?.target ?? '')) },
108
+ continueIr: { target: stripIdPrefix(String(readyOn.CONTINUE_IR?.target ?? '')) },
109
+ summarizeIr: { target: stripIdPrefix(String(readyOn.SUMMARIZE_IR?.target ?? '')) },
110
+ bossInterruptTargets: toArmArray(rootOn.BOSS_INTERRUPT).map((arm) =>
111
+ stripIdPrefix(String(arm.target ?? '')),
112
+ ),
113
+ };
114
+ }
115
+
116
+ function getRawConfig(machine: typeof codingMachine): RawConfig {
117
+ return (machine as unknown as { config: RawConfig }).config;
118
+ }
119
+
120
+ function toArmArray(value: unknown): RawArm[] {
121
+ if (value === undefined || value === null) return [];
122
+ return (Array.isArray(value) ? value : [value]) as RawArm[];
123
+ }
124
+
125
+ function stripIdPrefix(target: string): string {
126
+ return target.startsWith('#') ? target.slice(1) : target;
127
+ }
128
+
129
+ const alwaysTrue: TransitionGuard = () => true;