instant-ctrl-flow-logic 0.1.0 → 0.1.2
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/package.json +5 -6
- package/src/handlers.ts +11 -4
- package/src/loader.ts +42 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "instant-ctrl-flow-logic",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "InstantCtrlFlow 引擎:xnl bundle 加载 → 行为树编译 → 无状态单次执行",
|
|
6
6
|
"type": "module",
|
|
@@ -11,15 +11,14 @@
|
|
|
11
11
|
"./filesystem": "./src/filesystem.ts"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"xnl-core": "^0.1.8",
|
|
15
14
|
"depa-behavior-tree": "0.1.0",
|
|
16
|
-
"instant-ctrl-flow-contract": "0.1.0"
|
|
15
|
+
"instant-ctrl-flow-contract": "0.1.0",
|
|
16
|
+
"xnl-core": "^0.1.8"
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
19
|
"src"
|
|
20
20
|
],
|
|
21
21
|
"publishConfig": {
|
|
22
|
-
"access": "public"
|
|
23
|
-
"registry": "https://registry.npmjs.com"
|
|
22
|
+
"access": "public"
|
|
24
23
|
}
|
|
25
|
-
}
|
|
24
|
+
}
|
package/src/handlers.ts
CHANGED
|
@@ -93,6 +93,7 @@ export interface FlowDecisionContext {
|
|
|
93
93
|
export interface FlowDecisionHooks {
|
|
94
94
|
begin(ifPath: string): FlowDecisionContext;
|
|
95
95
|
record(decision: FlowBranchDecision): Promise<void> | void;
|
|
96
|
+
runtimeForNode?(nodeId: string, runtime: InstantCtrlFlowRuntime): InstantCtrlFlowRuntime;
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
class FlowFunctionLoader {
|
|
@@ -100,8 +101,13 @@ class FlowFunctionLoader {
|
|
|
100
101
|
constructor(
|
|
101
102
|
private spec: FlowBundleSpec,
|
|
102
103
|
private resolveCode: FlowCodeResolver,
|
|
104
|
+
private runtimeHooks?: FlowDecisionHooks,
|
|
103
105
|
) {}
|
|
104
106
|
|
|
107
|
+
runtimeForNode(nodeId: string, runtime: InstantCtrlFlowRuntime): InstantCtrlFlowRuntime {
|
|
108
|
+
return this.runtimeHooks?.runtimeForNode?.(nodeId, runtime) ?? runtime;
|
|
109
|
+
}
|
|
110
|
+
|
|
105
111
|
load(src: string, nodeId: string): Promise<FlowCodeFunction> {
|
|
106
112
|
let loaded = this.cache.get(src);
|
|
107
113
|
if (!loaded) {
|
|
@@ -128,7 +134,8 @@ function configOf(ctx: Ctx): NodeConfig {
|
|
|
128
134
|
async function callCode(ctx: Ctx, loader: FlowFunctionLoader, src: string): Promise<unknown> {
|
|
129
135
|
const state = flowStateOf(ctx);
|
|
130
136
|
const fn = await loader.load(src, ctx.node.Key);
|
|
131
|
-
|
|
137
|
+
const runtime = (ctx.runtime ?? {}) as InstantCtrlFlowRuntime;
|
|
138
|
+
return fn(loader.runtimeForNode(ctx.node.Key, runtime), state.current, configOf(ctx).config ?? {});
|
|
132
139
|
}
|
|
133
140
|
|
|
134
141
|
function fail(state: FlowExecutionState, error: unknown): BehaviorResult {
|
|
@@ -277,7 +284,7 @@ class StrategyStartHandler implements ActionHandler<CtrlFlowNodeType, INodeConfi
|
|
|
277
284
|
if (state.returned) return BehaviorResult.Success;
|
|
278
285
|
const key = String(configOf(ctx).fallbackKey);
|
|
279
286
|
state.current = state.fallbackInputs[key];
|
|
280
|
-
state.fault
|
|
287
|
+
delete state.fault;
|
|
281
288
|
return BehaviorResult.Success;
|
|
282
289
|
}
|
|
283
290
|
}
|
|
@@ -288,7 +295,7 @@ class FallbackStartHandler implements ActionHandler<CtrlFlowNodeType, INodeConfi
|
|
|
288
295
|
if (state.returned) return BehaviorResult.Success;
|
|
289
296
|
const key = String(configOf(ctx).fallbackKey);
|
|
290
297
|
state.fallbackInputs[key] = state.current;
|
|
291
|
-
state.fault
|
|
298
|
+
delete state.fault;
|
|
292
299
|
return BehaviorResult.Success;
|
|
293
300
|
}
|
|
294
301
|
}
|
|
@@ -440,7 +447,7 @@ export function createFlowHandlers(
|
|
|
440
447
|
executeBody?: FlowBodyExecutor,
|
|
441
448
|
executeTarget?: FlowTargetExecutor,
|
|
442
449
|
): Record<string, ActionHandler<CtrlFlowNodeType, INodeConfig>> {
|
|
443
|
-
const loader = new FlowFunctionLoader(spec, resolveCode);
|
|
450
|
+
const loader = new FlowFunctionLoader(spec, resolveCode, decisionHooks);
|
|
444
451
|
const activeDecisions = new Map<string, FlowDecisionContext>();
|
|
445
452
|
return {
|
|
446
453
|
'flow.Run': new RunHandler(loader),
|
package/src/loader.ts
CHANGED
|
@@ -107,9 +107,37 @@ export interface LoadResult {
|
|
|
107
107
|
diagnostics: FlowDiagnostic[];
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
export interface LoadCtrlFlowProfileSourcesOptions extends LoadFlowSourcesOptions {
|
|
111
|
+
profileRoot: string;
|
|
112
|
+
substrateForm: FlowForm;
|
|
113
|
+
}
|
|
114
|
+
|
|
110
115
|
export function loadFlowBundleFromSources(
|
|
111
116
|
sources: FlowSourceCollection,
|
|
112
117
|
options: LoadFlowSourcesOptions = {},
|
|
118
|
+
): LoadResult {
|
|
119
|
+
return loadCtrlFlowSources(sources, options);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function loadCtrlFlowProfileFromSources(
|
|
123
|
+
sources: FlowSourceCollection,
|
|
124
|
+
options: LoadCtrlFlowProfileSourcesOptions,
|
|
125
|
+
): LoadResult {
|
|
126
|
+
if (!options.profileRoot.trim()) {
|
|
127
|
+
return {
|
|
128
|
+
diagnostics: [{ code: 'invalid-profile-root', message: 'profile root must be a non-empty XNL tag' }],
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return loadCtrlFlowSources(sources, options, {
|
|
132
|
+
rootTag: options.profileRoot,
|
|
133
|
+
substrateForm: options.substrateForm,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function loadCtrlFlowSources(
|
|
138
|
+
sources: FlowSourceCollection,
|
|
139
|
+
options: LoadFlowSourcesOptions,
|
|
140
|
+
profile?: { rootTag: string; substrateForm: FlowForm },
|
|
113
141
|
): LoadResult {
|
|
114
142
|
const diagnostics: FlowDiagnostic[] = [];
|
|
115
143
|
const note = (code: string, message: string) => diagnostics.push({ code, message });
|
|
@@ -168,14 +196,25 @@ export function loadFlowBundleFromSources(
|
|
|
168
196
|
|
|
169
197
|
for (const tag of Object.keys(fileRoots)) noteLegacyTag(tag);
|
|
170
198
|
|
|
171
|
-
const form = FORMS.find((candidate) => fileRoots[candidate]);
|
|
199
|
+
const form = profile?.substrateForm ?? FORMS.find((candidate) => fileRoots[candidate]);
|
|
172
200
|
if (!form) {
|
|
173
201
|
if (noted.size) return { diagnostics };
|
|
174
202
|
note('no-container', `no container root found in ${options.baseUri ?? 'named source collection'}`);
|
|
175
203
|
return { diagnostics };
|
|
176
204
|
}
|
|
177
|
-
const
|
|
178
|
-
const
|
|
205
|
+
const rootTag = profile?.rootTag ?? form;
|
|
206
|
+
const rootEntry = fileRoots[rootTag];
|
|
207
|
+
if (!rootEntry) {
|
|
208
|
+
note(
|
|
209
|
+
profile ? 'profile-root-missing' : 'no-container',
|
|
210
|
+
profile
|
|
211
|
+
? `expected profile root <${rootTag}> in ${options.baseUri ?? 'named source collection'}`
|
|
212
|
+
: `no container root found in ${options.baseUri ?? 'named source collection'}`,
|
|
213
|
+
);
|
|
214
|
+
return { diagnostics };
|
|
215
|
+
}
|
|
216
|
+
const container = rootEntry.el;
|
|
217
|
+
const containerFile = rootEntry.file;
|
|
179
218
|
|
|
180
219
|
// Facet discovery is physical; toNodeSpec remains a pure XNL-to-contract projection.
|
|
181
220
|
const facets: Partial<Record<FacetName, XnlEl>> = {};
|