@compilr-dev/sdk 0.10.32 → 0.10.33
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/flow-runner/index.d.ts +12 -0
- package/dist/flow-runner/index.js +11 -0
- package/dist/flow-runner/runner.d.ts +88 -0
- package/dist/flow-runner/runner.js +212 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/package.json +1 -1
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flow Runner — Public API.
|
|
3
|
+
*
|
|
4
|
+
* Pure state-machine helpers for the `build_interactive_flow` DSL.
|
|
5
|
+
* Hosts hold the runtime state and call these helpers for transitions
|
|
6
|
+
* (next-resolution, branch evaluation, backtrack). The helpers are
|
|
7
|
+
* pure functions with no framework dependencies.
|
|
8
|
+
*
|
|
9
|
+
* See `runner.ts` for the implementation rationale and spec link.
|
|
10
|
+
*/
|
|
11
|
+
export { resolveNext, evaluateBranchCondition, walkPastBranches, lookupLabel, applyBacktrack, } from './runner.js';
|
|
12
|
+
export type { BacktrackResult } from './runner.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flow Runner — Public API.
|
|
3
|
+
*
|
|
4
|
+
* Pure state-machine helpers for the `build_interactive_flow` DSL.
|
|
5
|
+
* Hosts hold the runtime state and call these helpers for transitions
|
|
6
|
+
* (next-resolution, branch evaluation, backtrack). The helpers are
|
|
7
|
+
* pure functions with no framework dependencies.
|
|
8
|
+
*
|
|
9
|
+
* See `runner.ts` for the implementation rationale and spec link.
|
|
10
|
+
*/
|
|
11
|
+
export { resolveNext, evaluateBranchCondition, walkPastBranches, lookupLabel, applyBacktrack, } from './runner.js';
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flow Runner — Pure state-machine helpers for `build_interactive_flow`.
|
|
3
|
+
*
|
|
4
|
+
* Hosts (Desktop, CLI) hold the runtime state in whatever container fits
|
|
5
|
+
* their UI framework (React useState, BaseOverlayV2 state object, etc.)
|
|
6
|
+
* and call these helpers to compute transitions. The helpers are
|
|
7
|
+
* intentionally pure — no React, no DOM, no I/O — so they're trivially
|
|
8
|
+
* testable and reusable across renderers.
|
|
9
|
+
*
|
|
10
|
+
* Code lifted verbatim from `compilr-dev-desktop/src/renderer/src/
|
|
11
|
+
* components/flow/state/useFlowState.ts:297-409` (lines 240-272 also
|
|
12
|
+
* inform `applyBacktrack`). Desktop's `useFlowState` should re-export
|
|
13
|
+
* these in a follow-up commit; until then it keeps its local copies
|
|
14
|
+
* and these definitions are the canonical SDK ones.
|
|
15
|
+
*
|
|
16
|
+
* Spec: project-docs/00-requirements/compilr-dev-cli/interactive-flow-cli-spec.md
|
|
17
|
+
*/
|
|
18
|
+
import type { AnswerValue, BranchCondition, Flow, NextRef, NodeId, QuestionNode } from '../tools/interactive-flow-tool.js';
|
|
19
|
+
/**
|
|
20
|
+
* Result of an `applyBacktrack` call: the new path + the answer maps
|
|
21
|
+
* with everything-after-the-restored-node wiped (per spec §4).
|
|
22
|
+
*/
|
|
23
|
+
export interface BacktrackResult {
|
|
24
|
+
path: NodeId[];
|
|
25
|
+
answers: Record<NodeId, AnswerValue>;
|
|
26
|
+
answerLabels: Record<NodeId, AnswerValue>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Resolve a `NextRef` (+ optional answer that was just collected) to the
|
|
30
|
+
* single target nodeId. Returns `undefined` when the ref can't resolve —
|
|
31
|
+
* caller treats that as malformed-flow.
|
|
32
|
+
*
|
|
33
|
+
* Behaviour:
|
|
34
|
+
* - String ref: returned verbatim.
|
|
35
|
+
* - `{ byAnswer: { ... }, default }`:
|
|
36
|
+
* - For a scalar answer, look it up in `byAnswer`.
|
|
37
|
+
* - For a multi-select array, the first matching key wins.
|
|
38
|
+
* - Fall back to `default` if nothing matched.
|
|
39
|
+
*/
|
|
40
|
+
export declare function resolveNext(next: NextRef, answer?: AnswerValue): NodeId | undefined;
|
|
41
|
+
/**
|
|
42
|
+
* Evaluate a single branch condition against the collected answer map.
|
|
43
|
+
* Returns true iff the condition matches.
|
|
44
|
+
*
|
|
45
|
+
* - `equals`: scalar string equality
|
|
46
|
+
* - `includes`:
|
|
47
|
+
* - scalar string: equality (treat the answer as a 1-element list)
|
|
48
|
+
* - array (multi): true if the array contains the value
|
|
49
|
+
* - `notEquals`: scalar string inequality
|
|
50
|
+
*/
|
|
51
|
+
export declare function evaluateBranchCondition(condition: BranchCondition, answers: Record<NodeId, AnswerValue>): boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Walk past any leading branch nodes from the tip of `path`, appending
|
|
54
|
+
* each branch visited and finally appending the next non-branch node
|
|
55
|
+
* we land on. Returns the new path.
|
|
56
|
+
*
|
|
57
|
+
* The branch nodes ARE recorded in path (so the agent sees the routing
|
|
58
|
+
* trail), but they're never the current node when this function returns
|
|
59
|
+
* — the caller can safely render `path[path.length - 1]`.
|
|
60
|
+
*
|
|
61
|
+
* Safety: bounded by `Object.keys(flow.nodes).length + 1` to prevent
|
|
62
|
+
* infinite loops on malformed cyclic branches (SDK `validateFlow`
|
|
63
|
+
* should prevent this, but defensive).
|
|
64
|
+
*/
|
|
65
|
+
export declare function walkPastBranches(flow: Flow, path: NodeId[], answers: Record<NodeId, AnswerValue>): NodeId[];
|
|
66
|
+
/**
|
|
67
|
+
* Given a question node and the chosen answer, return the human-readable
|
|
68
|
+
* label(s). For `text` mode the label IS the answer. For `single` and
|
|
69
|
+
* `proposal`, look up the choice/option with the matching id. For
|
|
70
|
+
* `multi`, return an array of labels.
|
|
71
|
+
*
|
|
72
|
+
* Falls back to the answer id itself if no matching choice is found —
|
|
73
|
+
* keeps `answerLabels` populated even when validation didn't catch a
|
|
74
|
+
* mismatch.
|
|
75
|
+
*/
|
|
76
|
+
export declare function lookupLabel(node: QuestionNode, answer: AnswerValue): AnswerValue;
|
|
77
|
+
/**
|
|
78
|
+
* Compute the result of a backtrack ("go back one user-visible step").
|
|
79
|
+
*
|
|
80
|
+
* Pops path entries until the previous question/info node is exposed,
|
|
81
|
+
* then wipes every answer (and label) recorded at or after that node
|
|
82
|
+
* — per spec §4: "answers wiped downstream of any backtrack point".
|
|
83
|
+
*
|
|
84
|
+
* Returns `null` when there's no prior user-visible node (i.e. the
|
|
85
|
+
* user is already on `startNode`); the caller should treat that as a
|
|
86
|
+
* no-op (or, depending on UI, as a cancel signal).
|
|
87
|
+
*/
|
|
88
|
+
export declare function applyBacktrack(flow: Flow, path: NodeId[], answers: Record<NodeId, AnswerValue>, answerLabels: Record<NodeId, AnswerValue>): BacktrackResult | null;
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flow Runner — Pure state-machine helpers for `build_interactive_flow`.
|
|
3
|
+
*
|
|
4
|
+
* Hosts (Desktop, CLI) hold the runtime state in whatever container fits
|
|
5
|
+
* their UI framework (React useState, BaseOverlayV2 state object, etc.)
|
|
6
|
+
* and call these helpers to compute transitions. The helpers are
|
|
7
|
+
* intentionally pure — no React, no DOM, no I/O — so they're trivially
|
|
8
|
+
* testable and reusable across renderers.
|
|
9
|
+
*
|
|
10
|
+
* Code lifted verbatim from `compilr-dev-desktop/src/renderer/src/
|
|
11
|
+
* components/flow/state/useFlowState.ts:297-409` (lines 240-272 also
|
|
12
|
+
* inform `applyBacktrack`). Desktop's `useFlowState` should re-export
|
|
13
|
+
* these in a follow-up commit; until then it keeps its local copies
|
|
14
|
+
* and these definitions are the canonical SDK ones.
|
|
15
|
+
*
|
|
16
|
+
* Spec: project-docs/00-requirements/compilr-dev-cli/interactive-flow-cli-spec.md
|
|
17
|
+
*/
|
|
18
|
+
// =============================================================================
|
|
19
|
+
// Pure helpers
|
|
20
|
+
// =============================================================================
|
|
21
|
+
/**
|
|
22
|
+
* Resolve a `NextRef` (+ optional answer that was just collected) to the
|
|
23
|
+
* single target nodeId. Returns `undefined` when the ref can't resolve —
|
|
24
|
+
* caller treats that as malformed-flow.
|
|
25
|
+
*
|
|
26
|
+
* Behaviour:
|
|
27
|
+
* - String ref: returned verbatim.
|
|
28
|
+
* - `{ byAnswer: { ... }, default }`:
|
|
29
|
+
* - For a scalar answer, look it up in `byAnswer`.
|
|
30
|
+
* - For a multi-select array, the first matching key wins.
|
|
31
|
+
* - Fall back to `default` if nothing matched.
|
|
32
|
+
*/
|
|
33
|
+
export function resolveNext(next, answer) {
|
|
34
|
+
if (typeof next === 'string')
|
|
35
|
+
return next;
|
|
36
|
+
// Defensive against malformed runtime input (the test fixture passes
|
|
37
|
+
// null/undefined). TS proves this can't happen if you respect the
|
|
38
|
+
// declared type, but consumers may not.
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
40
|
+
if (!next || typeof next !== 'object')
|
|
41
|
+
return undefined;
|
|
42
|
+
// byAnswer map
|
|
43
|
+
const answerKey = typeof answer === 'string' ? answer : undefined;
|
|
44
|
+
if (answerKey !== undefined && answerKey in next.byAnswer) {
|
|
45
|
+
return next.byAnswer[answerKey];
|
|
46
|
+
}
|
|
47
|
+
// multi-select answer — first matching key wins
|
|
48
|
+
if (Array.isArray(answer)) {
|
|
49
|
+
for (const a of answer) {
|
|
50
|
+
if (a in next.byAnswer)
|
|
51
|
+
return next.byAnswer[a];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return next.default;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Evaluate a single branch condition against the collected answer map.
|
|
58
|
+
* Returns true iff the condition matches.
|
|
59
|
+
*
|
|
60
|
+
* - `equals`: scalar string equality
|
|
61
|
+
* - `includes`:
|
|
62
|
+
* - scalar string: equality (treat the answer as a 1-element list)
|
|
63
|
+
* - array (multi): true if the array contains the value
|
|
64
|
+
* - `notEquals`: scalar string inequality
|
|
65
|
+
*/
|
|
66
|
+
export function evaluateBranchCondition(condition, answers) {
|
|
67
|
+
const value = answers[condition.questionId];
|
|
68
|
+
// Defensive — Record<K,V>[K] is `V` (not `V|undefined`) in non-strict-
|
|
69
|
+
// index mode, but the question may simply not have been answered yet.
|
|
70
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
71
|
+
if (value === undefined)
|
|
72
|
+
return false;
|
|
73
|
+
if ('equals' in condition) {
|
|
74
|
+
return typeof value === 'string' && value === condition.equals;
|
|
75
|
+
}
|
|
76
|
+
if ('includes' in condition) {
|
|
77
|
+
if (typeof value === 'string')
|
|
78
|
+
return value === condition.includes;
|
|
79
|
+
return value.includes(condition.includes);
|
|
80
|
+
}
|
|
81
|
+
if ('notEquals' in condition) {
|
|
82
|
+
return typeof value === 'string' && value !== condition.notEquals;
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Walk past any leading branch nodes from the tip of `path`, appending
|
|
88
|
+
* each branch visited and finally appending the next non-branch node
|
|
89
|
+
* we land on. Returns the new path.
|
|
90
|
+
*
|
|
91
|
+
* The branch nodes ARE recorded in path (so the agent sees the routing
|
|
92
|
+
* trail), but they're never the current node when this function returns
|
|
93
|
+
* — the caller can safely render `path[path.length - 1]`.
|
|
94
|
+
*
|
|
95
|
+
* Safety: bounded by `Object.keys(flow.nodes).length + 1` to prevent
|
|
96
|
+
* infinite loops on malformed cyclic branches (SDK `validateFlow`
|
|
97
|
+
* should prevent this, but defensive).
|
|
98
|
+
*/
|
|
99
|
+
export function walkPastBranches(flow, path, answers) {
|
|
100
|
+
const result = [...path];
|
|
101
|
+
let guard = Object.keys(flow.nodes).length + 1;
|
|
102
|
+
while (guard-- > 0) {
|
|
103
|
+
const tip = result[result.length - 1];
|
|
104
|
+
const node = flow.nodes[tip];
|
|
105
|
+
// Defensive — `flow.nodes[tip]` is typed as `Node` but the runtime
|
|
106
|
+
// value may be undefined if a NextRef pointed to a non-existent id.
|
|
107
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
108
|
+
if (!node || node.type !== 'branch')
|
|
109
|
+
return result;
|
|
110
|
+
// Evaluate routes in order; first match wins
|
|
111
|
+
let target = node.default;
|
|
112
|
+
for (const route of node.routes) {
|
|
113
|
+
if (evaluateBranchCondition(route.when, answers)) {
|
|
114
|
+
target = route.goto;
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (!target || !(target in flow.nodes))
|
|
119
|
+
return result; // malformed; stop walking
|
|
120
|
+
result.push(target);
|
|
121
|
+
}
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Given a question node and the chosen answer, return the human-readable
|
|
126
|
+
* label(s). For `text` mode the label IS the answer. For `single` and
|
|
127
|
+
* `proposal`, look up the choice/option with the matching id. For
|
|
128
|
+
* `multi`, return an array of labels.
|
|
129
|
+
*
|
|
130
|
+
* Falls back to the answer id itself if no matching choice is found —
|
|
131
|
+
* keeps `answerLabels` populated even when validation didn't catch a
|
|
132
|
+
* mismatch.
|
|
133
|
+
*/
|
|
134
|
+
export function lookupLabel(node, answer) {
|
|
135
|
+
const mode = node.input.mode;
|
|
136
|
+
if (mode === 'text') {
|
|
137
|
+
return answer;
|
|
138
|
+
}
|
|
139
|
+
if (mode === 'single') {
|
|
140
|
+
const id = typeof answer === 'string' ? answer : answer[0];
|
|
141
|
+
// Defensive — empty array answer
|
|
142
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
143
|
+
if (id === undefined)
|
|
144
|
+
return answer;
|
|
145
|
+
const found = node.input.choices.find((c) => c.id === id);
|
|
146
|
+
return found?.label ?? id;
|
|
147
|
+
}
|
|
148
|
+
if (mode === 'multi') {
|
|
149
|
+
const ids = Array.isArray(answer) ? answer : [answer];
|
|
150
|
+
return ids.map((id) => {
|
|
151
|
+
if (node.input.mode !== 'multi')
|
|
152
|
+
return id;
|
|
153
|
+
const found = node.input.choices.find((c) => c.id === id);
|
|
154
|
+
return found?.label ?? id;
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
// mode === 'proposal' — narrowed by the if-chain above
|
|
158
|
+
const id = typeof answer === 'string' ? answer : answer[0];
|
|
159
|
+
// Defensive — empty array answer
|
|
160
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
161
|
+
if (id === undefined)
|
|
162
|
+
return answer;
|
|
163
|
+
const found = node.input.options.find((o) => o.id === id);
|
|
164
|
+
return found?.label ?? id;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Compute the result of a backtrack ("go back one user-visible step").
|
|
168
|
+
*
|
|
169
|
+
* Pops path entries until the previous question/info node is exposed,
|
|
170
|
+
* then wipes every answer (and label) recorded at or after that node
|
|
171
|
+
* — per spec §4: "answers wiped downstream of any backtrack point".
|
|
172
|
+
*
|
|
173
|
+
* Returns `null` when there's no prior user-visible node (i.e. the
|
|
174
|
+
* user is already on `startNode`); the caller should treat that as a
|
|
175
|
+
* no-op (or, depending on UI, as a cancel signal).
|
|
176
|
+
*/
|
|
177
|
+
export function applyBacktrack(flow, path, answers, answerLabels) {
|
|
178
|
+
let targetIdx = path.length - 1;
|
|
179
|
+
for (let i = path.length - 2; i >= 0; i--) {
|
|
180
|
+
const id = path[i];
|
|
181
|
+
const node = flow.nodes[id];
|
|
182
|
+
// Defensive against missing-node entries in path (shouldn't happen but
|
|
183
|
+
// path entries originate from runtime, not the type system)
|
|
184
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
185
|
+
if (node && node.type !== 'branch') {
|
|
186
|
+
targetIdx = i;
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (targetIdx === path.length - 1)
|
|
191
|
+
return null; // no prior visible node
|
|
192
|
+
const newPath = path.slice(0, targetIdx + 1);
|
|
193
|
+
// Wipe answers (and labels) for nodes from targetIdx onward (inclusive —
|
|
194
|
+
// the user is re-entering that node). Build fresh maps rather than
|
|
195
|
+
// `delete` keys (lint forbids dynamic delete).
|
|
196
|
+
const wipedIds = new Set(path.slice(targetIdx));
|
|
197
|
+
const wipedAnswers = {};
|
|
198
|
+
for (const [id, val] of Object.entries(answers)) {
|
|
199
|
+
if (!wipedIds.has(id))
|
|
200
|
+
wipedAnswers[id] = val;
|
|
201
|
+
}
|
|
202
|
+
const wipedLabels = {};
|
|
203
|
+
for (const [id, val] of Object.entries(answerLabels)) {
|
|
204
|
+
if (!wipedIds.has(id))
|
|
205
|
+
wipedLabels[id] = val;
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
path: newPath,
|
|
209
|
+
answers: wipedAnswers,
|
|
210
|
+
answerLabels: wipedLabels,
|
|
211
|
+
};
|
|
212
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -84,6 +84,8 @@ export type { PermissionRule, PermissionMode, PermissionLevel } from './permissi
|
|
|
84
84
|
export { DEFAULT_DELEGATION_CONFIG } from './delegation.js';
|
|
85
85
|
export { FileEpisodeStore, EpisodeRecorder, isSignificantWork, extractAffectedFiles, extractLinesChanged, queryWorkAtRisk, buildWorkSummaryContent, updateWorkSummaryAnchor, } from './episodes/index.js';
|
|
86
86
|
export type { FileEpisodeStoreOptions, EpisodeRecorderConfig, WorkSummaryAnchorConfig, PendingToolSignal, EpisodeFile, } from './episodes/index.js';
|
|
87
|
+
export { resolveNext, evaluateBranchCondition, walkPastBranches, lookupLabel, applyBacktrack, } from './flow-runner/index.js';
|
|
88
|
+
export type { BacktrackResult } from './flow-runner/index.js';
|
|
87
89
|
export { readMCPConfigFile, writeMCPConfigFile, resolveServerEntry, loadMCPServers, saveMCPServerEntry, deleteMCPServerEntry, getServerNames, } from './mcp-config.js';
|
|
88
90
|
export type { MCPServerEntry, MCPConfigFile, ResolvedMCPServer } from './mcp-config.js';
|
|
89
91
|
export { generateProject, isGitConfigured, generateCompilrMd, generateConfigJson, generateReadmeMd, generateCodingStandardsMd, generatePackageJson, generateTsconfig, generateGitignore, generateCompilrMdForImport, detectProjectInfo, detectGitInfo, prettifyName, getLanguageLabel, getFrameworkLabel, validateImportPath, isValidProjectName, projectExists, TECH_STACK_LABELS, CODING_STANDARDS_LABELS, REPO_PATTERN_LABELS, WORKFLOW_VERSION, } from './project-generator/index.js';
|
package/dist/index.js
CHANGED
|
@@ -206,6 +206,10 @@ export { DEFAULT_DELEGATION_CONFIG } from './delegation.js';
|
|
|
206
206
|
// =============================================================================
|
|
207
207
|
export { FileEpisodeStore, EpisodeRecorder, isSignificantWork, extractAffectedFiles, extractLinesChanged, queryWorkAtRisk, buildWorkSummaryContent, updateWorkSummaryAnchor, } from './episodes/index.js';
|
|
208
208
|
// =============================================================================
|
|
209
|
+
// Flow Runner — `build_interactive_flow` State Machine Helpers
|
|
210
|
+
// =============================================================================
|
|
211
|
+
export { resolveNext, evaluateBranchCondition, walkPastBranches, lookupLabel, applyBacktrack, } from './flow-runner/index.js';
|
|
212
|
+
// =============================================================================
|
|
209
213
|
// Shared MCP Configuration
|
|
210
214
|
// =============================================================================
|
|
211
215
|
export { readMCPConfigFile, writeMCPConfigFile, resolveServerEntry, loadMCPServers, saveMCPServerEntry, deleteMCPServerEntry, getServerNames, } from './mcp-config.js';
|