work-ctrl-flow-logic 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.
- package/package.json +26 -0
- package/src/browser.ts +1 -0
- package/src/engine.ts +1255 -0
- package/src/filesystem.ts +34 -0
- package/src/index.ts +4 -0
- package/src/instance.ts +104 -0
- package/src/stores.ts +44 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import {
|
|
4
|
+
createFilesystemFlowCodeResolver,
|
|
5
|
+
loadFlowBundle,
|
|
6
|
+
} from 'instant-ctrl-flow-logic/filesystem';
|
|
7
|
+
import { WorkCtrlFlowEngine, WorkCtrlFlowLoadError, type WorkCtrlFlowEngineOptions } from './engine.js';
|
|
8
|
+
import { annotateFlowFile, materializeInstance } from './instance.js';
|
|
9
|
+
|
|
10
|
+
export interface WorkCtrlFlowDirectoryOptions
|
|
11
|
+
extends Omit<WorkCtrlFlowEngineOptions, 'definition' | 'view'> {
|
|
12
|
+
bundleDir: string;
|
|
13
|
+
instancesDir?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Directory convenience only; loading and execution delegate to source/spec core APIs. */
|
|
17
|
+
export function createWorkCtrlFlowEngineFromDirectory(options: WorkCtrlFlowDirectoryOptions): WorkCtrlFlowEngine {
|
|
18
|
+
const result = loadFlowBundle(options.bundleDir);
|
|
19
|
+
const hard = result.diagnostics.filter((diagnostic) => diagnostic.code !== 'parser-warning');
|
|
20
|
+
if (!result.spec || hard.length) {
|
|
21
|
+
throw new WorkCtrlFlowLoadError(result.diagnostics.map((item) => item.code), result.diagnostics.map((item) => item.message).join('; '));
|
|
22
|
+
}
|
|
23
|
+
return new WorkCtrlFlowEngine({
|
|
24
|
+
...options,
|
|
25
|
+
definition: { spec: result.spec, resolveCode: createFilesystemFlowCodeResolver() },
|
|
26
|
+
...(options.instancesDir
|
|
27
|
+
? { view: { annotateTree: (treeId, view) => {
|
|
28
|
+
const instanceDir = path.join(options.instancesDir!, treeId);
|
|
29
|
+
if (!fs.existsSync(instanceDir)) materializeInstance(options.bundleDir, instanceDir);
|
|
30
|
+
annotateFlowFile(instanceDir, view);
|
|
31
|
+
} } }
|
|
32
|
+
: {}),
|
|
33
|
+
});
|
|
34
|
+
}
|
package/src/index.ts
ADDED
package/src/instance.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 流程实例物化 + status 标注视图(用户定稿设计):
|
|
3
|
+
* - 实例 = 定义 bundle 的冻结拷贝(含脚本),实例终身只从拷贝执行(定义修改不影响在途实例)。
|
|
4
|
+
* - status 是引擎单向标注的视图,快照仍是真源;标注写在节点 { } 属性块里,用文本手术保留定义注释。
|
|
5
|
+
* - 保留视图键:status / iterations / runStatus / tickNo(编译期剔除,见 instant-ctrl-flow compile)。
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import type { NodeAnnotation, NodeViewStatus, TreeViewAnnotation } from 'work-ctrl-flow-contract';
|
|
10
|
+
|
|
11
|
+
export type { NodeAnnotation, NodeViewStatus, TreeViewAnnotation };
|
|
12
|
+
|
|
13
|
+
/** 把定义 bundle 递归拷贝为实例目录(冻结) */
|
|
14
|
+
export function materializeInstance(definitionDir: string, instanceDir: string): void {
|
|
15
|
+
fs.mkdirSync(path.dirname(instanceDir), { recursive: true });
|
|
16
|
+
fs.cpSync(definitionDir, instanceDir, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const VIEW_ATTR_RE = /\s*(?:status|iterations|runStatus|tickNo)\s*=\s*(?:"[^"]*"|[\w.]+)/g;
|
|
20
|
+
|
|
21
|
+
/** 移除既往注入的视图键(仅 manifest.xnl 使用;task.space 的 status 是真实 DSL 字段,走 replace 路径) */
|
|
22
|
+
export function stripViewAttrs(text: string): string {
|
|
23
|
+
let out = text.replace(VIEW_ATTR_RE, '');
|
|
24
|
+
// 清理注入后可能残留的空属性块 "{ }"(仅当它是我们注入的——定义里不写空块)
|
|
25
|
+
out = out.replace(/\s*\{\s*\}(?=\s*[([>])/g, '');
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const fmt = (v: string | number) => (typeof v === 'number' ? String(v) : `"${v}"`);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 在 `<Tag #id` 之后的 { } 属性块头部注入键值(无属性块则新建)。
|
|
33
|
+
* 依赖校验器保证的不变量:树内 #id 唯一。
|
|
34
|
+
*/
|
|
35
|
+
function injectAttrs(text: string, id: string, entries: Record<string, string | number>): string {
|
|
36
|
+
const pairs = Object.entries(entries)
|
|
37
|
+
.map(([k, v]) => `${k} = ${fmt(v)}`)
|
|
38
|
+
.join(' ');
|
|
39
|
+
if (!pairs) return text;
|
|
40
|
+
const esc = id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
41
|
+
const re = new RegExp(`(<[\\w.]+\\s+#${esc})(?![\\w.-])`);
|
|
42
|
+
const m = re.exec(text);
|
|
43
|
+
if (!m) return text; // 节点在实例树中不存在(不应发生)——视图尽力而为,不抛错
|
|
44
|
+
let i = m.index + m[0].length;
|
|
45
|
+
// 跳过元数据位 key=value(tree 节点通常没有;容错处理)
|
|
46
|
+
const metaRe = /^\s+[A-Za-z_][\w.]*\s*=\s*(?:"[^"]*"|[\w.]+)/y;
|
|
47
|
+
for (;;) {
|
|
48
|
+
metaRe.lastIndex = i;
|
|
49
|
+
const mm = metaRe.exec(text);
|
|
50
|
+
if (!mm) break;
|
|
51
|
+
i = metaRe.lastIndex;
|
|
52
|
+
}
|
|
53
|
+
const rest = text.slice(i);
|
|
54
|
+
const nextStructural = rest.search(/[{([>]/);
|
|
55
|
+
const ch = rest[nextStructural];
|
|
56
|
+
if (ch === '{') {
|
|
57
|
+
const at = i + nextStructural + 1;
|
|
58
|
+
return text.slice(0, at) + ` ${pairs}` + text.slice(at);
|
|
59
|
+
}
|
|
60
|
+
const at = i + nextStructural;
|
|
61
|
+
return text.slice(0, at) + `{ ${pairs} } ` + text.slice(at);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 把状态视图写回实例 manifest.xnl(幂等:先剥离旧视图键再注入) */
|
|
65
|
+
export function annotateFlowFile(instanceDir: string, view: TreeViewAnnotation): void {
|
|
66
|
+
const file = path.join(instanceDir, 'manifest.xnl');
|
|
67
|
+
if (!fs.existsSync(file)) return;
|
|
68
|
+
let text = stripViewAttrs(fs.readFileSync(file, 'utf8'));
|
|
69
|
+
const flowRoot = /(<(?:WorkCtrlFlow|BPCtrlFlow)(?:\s+#[\w.-]+)?)/.exec(text);
|
|
70
|
+
if (flowRoot) {
|
|
71
|
+
const at = flowRoot.index + flowRoot[0].length;
|
|
72
|
+
const rest = text.slice(at);
|
|
73
|
+
const next = rest.search(/[{([>]/);
|
|
74
|
+
if (rest[next] === '{') {
|
|
75
|
+
const p = at + next + 1;
|
|
76
|
+
text = text.slice(0, p) + ` runStatus = "${view.runStatus}" tickNo = ${view.tickNo}` + text.slice(p);
|
|
77
|
+
} else {
|
|
78
|
+
const p = at + next;
|
|
79
|
+
text = text.slice(0, p) + `{ runStatus = "${view.runStatus}" tickNo = ${view.tickNo} } ` + text.slice(p);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
for (const [id, a] of Object.entries(view.nodes)) {
|
|
83
|
+
const entries: Record<string, string | number> = { status: a.status };
|
|
84
|
+
if (a.iterations !== undefined) entries.iterations = a.iterations;
|
|
85
|
+
text = injectAttrs(text, id, entries);
|
|
86
|
+
}
|
|
87
|
+
const tmp = file + '.tmp';
|
|
88
|
+
fs.writeFileSync(tmp, text);
|
|
89
|
+
fs.renameSync(tmp, file);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** BPCtrlFlow:把任务状态机现值同步进实例 task.space.xnl(替换既有 status 值;真源在 TaskSpaceStore) */
|
|
93
|
+
export function annotateTaskSpaceFile(instanceDir: string, taskStatuses: Record<string, string>): void {
|
|
94
|
+
const file = path.join(instanceDir, 'task.space.xnl');
|
|
95
|
+
if (!fs.existsSync(file)) return;
|
|
96
|
+
let text = fs.readFileSync(file, 'utf8');
|
|
97
|
+
for (const [id, status] of Object.entries(taskStatuses)) {
|
|
98
|
+
const esc = id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
99
|
+
text = text.replace(new RegExp(`(<Task\\s+#${esc}(?![\\w.-])[\\s\\S]*?status\\s*=\\s*")[A-Za-z_]+(")`), `$1${status}$2`);
|
|
100
|
+
}
|
|
101
|
+
const tmp = file + '.tmp';
|
|
102
|
+
fs.writeFileSync(tmp, text);
|
|
103
|
+
fs.renameSync(tmp, file);
|
|
104
|
+
}
|
package/src/stores.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkCtrlFlowStore 实现:文件系统打底 + 内存(测试)。单写者假设(design A1)。
|
|
3
|
+
*/
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import type { WorkCtrlFlowSnapshot, WorkCtrlFlowStore } from 'work-ctrl-flow-contract';
|
|
7
|
+
|
|
8
|
+
export class InMemoryWorkCtrlFlowStore implements WorkCtrlFlowStore {
|
|
9
|
+
private snapshots = new Map<string, WorkCtrlFlowSnapshot>();
|
|
10
|
+
|
|
11
|
+
async load(treeId: string): Promise<WorkCtrlFlowSnapshot | undefined> {
|
|
12
|
+
const s = this.snapshots.get(treeId);
|
|
13
|
+
return s ? structuredClone(s) : undefined;
|
|
14
|
+
}
|
|
15
|
+
async save(snapshot: WorkCtrlFlowSnapshot): Promise<void> {
|
|
16
|
+
this.snapshots.set(snapshot.treeId, structuredClone(snapshot));
|
|
17
|
+
}
|
|
18
|
+
async remove(treeId: string): Promise<void> {
|
|
19
|
+
this.snapshots.delete(treeId);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class FileWorkCtrlFlowStore implements WorkCtrlFlowStore {
|
|
24
|
+
constructor(private dir: string) {
|
|
25
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
private file(treeId: string) {
|
|
28
|
+
return path.join(this.dir, `${encodeURIComponent(treeId)}.snapshot.json`);
|
|
29
|
+
}
|
|
30
|
+
async load(treeId: string): Promise<WorkCtrlFlowSnapshot | undefined> {
|
|
31
|
+
const f = this.file(treeId);
|
|
32
|
+
if (!fs.existsSync(f)) return undefined;
|
|
33
|
+
return JSON.parse(fs.readFileSync(f, 'utf8')) as WorkCtrlFlowSnapshot;
|
|
34
|
+
}
|
|
35
|
+
async save(snapshot: WorkCtrlFlowSnapshot): Promise<void> {
|
|
36
|
+
const f = this.file(snapshot.treeId);
|
|
37
|
+
const tmp = f + '.tmp';
|
|
38
|
+
fs.writeFileSync(tmp, JSON.stringify(snapshot, null, 2));
|
|
39
|
+
fs.renameSync(tmp, f);
|
|
40
|
+
}
|
|
41
|
+
async remove(treeId: string): Promise<void> {
|
|
42
|
+
fs.rmSync(this.file(treeId), { force: true });
|
|
43
|
+
}
|
|
44
|
+
}
|