work-ctrl-flow-logic 0.1.1 → 0.1.3

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 (2) hide show
  1. package/package.json +5 -4
  2. package/src/instance.ts +71 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "work-ctrl-flow-logic",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "private": false,
5
5
  "description": "WorkCtrlFlow 引擎:可中断可恢复的动态工作流(快照重放 + WaitHandle/ResumeSignal)",
6
6
  "type": "module",
@@ -12,9 +12,10 @@
12
12
  },
13
13
  "dependencies": {
14
14
  "depa-behavior-tree": "0.1.0",
15
- "instant-ctrl-flow-contract": "0.1.0",
16
- "instant-ctrl-flow-logic": "0.1.2",
17
- "work-ctrl-flow-contract": "0.1.1"
15
+ "flow-step-space-logic": "0.1.1",
16
+ "instant-ctrl-flow-contract": "0.1.2",
17
+ "instant-ctrl-flow-logic": "0.1.4",
18
+ "work-ctrl-flow-contract": "0.1.3"
18
19
  },
19
20
  "files": [
20
21
  "src"
package/src/instance.ts CHANGED
@@ -8,6 +8,12 @@ import fs from 'node:fs';
8
8
  import path from 'node:path';
9
9
  import { createHash, randomUUID } from 'node:crypto';
10
10
  import { loadFlowBundle } from 'instant-ctrl-flow-logic/filesystem';
11
+ import { loadCtrlFlowProfileFromSources } from 'instant-ctrl-flow-logic';
12
+ import type { FlowNodeSpec } from 'instant-ctrl-flow-contract';
13
+ import {
14
+ assembleDefinitionStepProfileDirectory,
15
+ createFilesystemDefinitionStepProfileRuntime,
16
+ } from 'flow-step-space-logic/filesystem';
11
17
  import {
12
18
  FLOW_INSTANCE_SCHEMA_VERSION,
13
19
  type FlowDefinitionProvenance,
@@ -50,6 +56,48 @@ export interface MaterializedFlowInstance {
50
56
  readonly created: boolean;
51
57
  }
52
58
 
59
+ /** Definition-admission projection used by the checkpoint owner for Ctrl membership. */
60
+ export function loadFrozenCtrlDefinitionStepIds(
61
+ definitionDir: string,
62
+ profileKind: 'WorkCtrlFlow' | 'AICtrlWorkflow',
63
+ ): readonly string[] {
64
+ const runtime = createFilesystemDefinitionStepProfileRuntime({ rootDir: definitionDir });
65
+ const assembled = assembleDefinitionStepProfileDirectory(runtime, {}, { profileRoot: profileKind });
66
+ if (assembled.diagnostics.length > 0) {
67
+ throw new FlowInstanceMaterializationError(
68
+ 'instance-definition-invalid',
69
+ `frozen ${profileKind} StepSpace is invalid: ${assembled.diagnostics.map((item) => item.code).join(', ')}`,
70
+ );
71
+ }
72
+ const profileSpec = profileKind === 'AICtrlWorkflow'
73
+ ? loadCtrlFlowProfileFromSources(
74
+ Object.fromEntries((assembled.sources as readonly ({ readonly ref: string; readonly content: string } | { readonly name: string; readonly content: string })[])
75
+ .map((source) => ['name' in source ? source.name : source.ref, source.content])),
76
+ { profileRoot: 'AICtrlWorkflow', substrateForm: 'WorkCtrlFlow' },
77
+ ).spec
78
+ : undefined;
79
+ // The AICtrl runtime can also be bound to an already-canonical WorkCtrl
80
+ // definition. That frozen substrate binding remains the membership authority.
81
+ const spec = profileSpec ?? loadFlowBundle(definitionDir).spec;
82
+ if (!spec) {
83
+ throw new FlowInstanceMaterializationError('instance-definition-invalid', `frozen ${profileKind} binding is invalid`);
84
+ }
85
+ const ids: string[] = [];
86
+ const visit = (node: FlowNodeSpec): void => {
87
+ if (node.id) ids.push(node.id);
88
+ node.children.forEach(visit);
89
+ Object.values(node.sections).forEach(visit);
90
+ };
91
+ spec.statements.forEach(visit);
92
+ if (ids.length === 0 || new Set(ids).size !== ids.length) {
93
+ throw new FlowInstanceMaterializationError(
94
+ 'instance-definition-step-identities-invalid',
95
+ 'frozen Ctrl binding must expose unique executable step identities',
96
+ );
97
+ }
98
+ return Object.freeze(ids);
99
+ }
100
+
53
101
  export class FlowInstanceMaterializationError extends Error {
54
102
  constructor(public readonly code: string, message: string) {
55
103
  super(message);
@@ -686,6 +734,29 @@ export function annotateFlowFile(instanceDir: string, view: TreeViewAnnotation):
686
734
  const tmp = file + '.tmp';
687
735
  fs.writeFileSync(tmp, text);
688
736
  fs.renameSync(tmp, file);
737
+
738
+ const assembled = assembleDefinitionStepProfileDirectory(
739
+ createFilesystemDefinitionStepProfileRuntime({ rootDir: instanceDir }),
740
+ {},
741
+ { profileRoot: 'WorkCtrlFlow' },
742
+ );
743
+ if (!assembled.forest || assembled.diagnostics.length > 0) return;
744
+ for (const stepId of assembled.forest.stepOrder) {
745
+ const stepFile = path.join(instanceDir, ...assembled.forest.steps[stepId].sourceRef.split('/'));
746
+ let stepText = stripViewAttrs(fs.readFileSync(stepFile, 'utf8'));
747
+ const coreOffset = stepText.indexOf('<Core');
748
+ const prefix = coreOffset >= 0 ? stepText.slice(0, coreOffset) : '';
749
+ let coreText = coreOffset >= 0 ? stepText.slice(coreOffset) : stepText;
750
+ for (const [id, annotation] of Object.entries(view.nodes)) {
751
+ const entries: Record<string, string | number> = { status: annotation.status };
752
+ if (annotation.iterations !== undefined) entries.iterations = annotation.iterations;
753
+ coreText = injectAttrs(coreText, id, entries);
754
+ }
755
+ stepText = `${prefix}${coreText}`;
756
+ const stepTmp = `${stepFile}.tmp`;
757
+ fs.writeFileSync(stepTmp, stepText);
758
+ fs.renameSync(stepTmp, stepFile);
759
+ }
689
760
  }
690
761
 
691
762
  /** BPCtrlFlow:把任务状态机现值同步进实例 task.space.xnl(替换既有 status 值;真源在 TaskSpaceStore) */