task-manager-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 ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "task-manager-logic",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "任务管理实现:状态机、组聚合、任务树存储",
6
+ "type": "module",
7
+ "main": "./src/index.ts",
8
+ "exports": {
9
+ ".": "./src/index.ts"
10
+ },
11
+ "dependencies": {
12
+ "task-manager-contract": "0.1.0"
13
+ },
14
+ "files": [
15
+ "src"
16
+ ],
17
+ "publishConfig": {
18
+ "access": "public",
19
+ "registry": "https://registry.npmjs.com"
20
+ }
21
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './statusMachine.js';
2
+ export * from './tree.js';
3
+ export * from './stores.js';
@@ -0,0 +1,43 @@
1
+ /**
2
+ * 任务状态机(BP-1):转移合法性 + started_at/ended_at 语义。
3
+ */
4
+ import {
5
+ OPERATION_TARGET,
6
+ TASK_TRANSITIONS,
7
+ TERMINAL_STATUSES,
8
+ type TaskDTO,
9
+ type TaskOperation,
10
+ type TaskStatus,
11
+ } from 'task-manager-contract';
12
+
13
+ export class TaskTransitionError extends Error {
14
+ constructor(public taskId: string, public from: TaskStatus, public to: TaskStatus) {
15
+ super(`illegal task transition ${from} -> ${to} (task ${taskId})`);
16
+ }
17
+ }
18
+
19
+ export function canTransition(from: TaskStatus, to: TaskStatus): boolean {
20
+ return TASK_TRANSITIONS[from].includes(to);
21
+ }
22
+
23
+ export function isTerminal(status: TaskStatus): boolean {
24
+ return TERMINAL_STATUSES.has(status);
25
+ }
26
+
27
+ /** 应用一次人工操作;非法转移抛 TaskTransitionError。返回同一 task 对象(原地更新)。 */
28
+ export function applyOperation(
29
+ task: TaskDTO,
30
+ operation: TaskOperation,
31
+ operatorAccount: string,
32
+ nowMs: number,
33
+ ): TaskDTO {
34
+ const to = OPERATION_TARGET[operation];
35
+ if (!canTransition(task.status, to)) throw new TaskTransitionError(task.id, task.status, to);
36
+ const from = task.status;
37
+ task.status = to;
38
+ task.operatorAccount = operatorAccount;
39
+ if (to === 'Active' && from !== 'Active') task.startedAtMs ??= nowMs;
40
+ if (isTerminal(to)) task.endedAtMs = nowMs;
41
+ if (operation === 'delegate' || operation === 'forward') task.transferredBy = operatorAccount;
42
+ return task;
43
+ }
package/src/stores.ts ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * 任务树外置存储:内存 + 文件(JSON)。
3
+ */
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import type { TaskSpaceDTO, TaskSpaceStore } from 'task-manager-contract';
7
+
8
+ export class InMemoryTaskSpaceStore implements TaskSpaceStore {
9
+ private spaces = new Map<string, TaskSpaceDTO>();
10
+ async load(spaceId: string): Promise<TaskSpaceDTO | undefined> {
11
+ const s = this.spaces.get(spaceId);
12
+ return s ? structuredClone(s) : undefined;
13
+ }
14
+ async save(spaceId: string, space: TaskSpaceDTO): Promise<void> {
15
+ this.spaces.set(spaceId, structuredClone(space));
16
+ }
17
+ async remove(spaceId: string): Promise<void> {
18
+ this.spaces.delete(spaceId);
19
+ }
20
+ }
21
+
22
+ export class FileTaskSpaceStore implements TaskSpaceStore {
23
+ constructor(private dir: string) {
24
+ fs.mkdirSync(dir, { recursive: true });
25
+ }
26
+ private file(spaceId: string) {
27
+ return path.join(this.dir, `${encodeURIComponent(spaceId)}.task-space.json`);
28
+ }
29
+ async load(spaceId: string): Promise<TaskSpaceDTO | undefined> {
30
+ const f = this.file(spaceId);
31
+ if (!fs.existsSync(f)) return undefined;
32
+ return JSON.parse(fs.readFileSync(f, 'utf8')) as TaskSpaceDTO;
33
+ }
34
+ async save(spaceId: string, space: TaskSpaceDTO): Promise<void> {
35
+ const f = this.file(spaceId);
36
+ fs.writeFileSync(f + '.tmp', JSON.stringify(space, null, 2));
37
+ fs.renameSync(f + '.tmp', f);
38
+ }
39
+ async remove(spaceId: string): Promise<void> {
40
+ fs.rmSync(this.file(spaceId), { force: true });
41
+ }
42
+ }
package/src/tree.ts ADDED
@@ -0,0 +1,100 @@
1
+ /**
2
+ * 任务树查找与组聚合(executionMode 语义,BP-2/BP-4)。
3
+ */
4
+ import type { ExecutionMode, TaskDTO, TaskGroupDTO, TaskSpaceDTO, TaskStatus, TaskTreeNode } from 'task-manager-contract';
5
+ import { isTerminal } from './statusMachine.js';
6
+
7
+ export function findNode(space: TaskSpaceDTO, id: string): TaskTreeNode | undefined {
8
+ const walk = (nodes: TaskTreeNode[]): TaskTreeNode | undefined => {
9
+ for (const n of nodes) {
10
+ if (n.id === id) return n;
11
+ if (n.kind === 'group') {
12
+ const hit = walk(n.children);
13
+ if (hit) return hit;
14
+ }
15
+ }
16
+ return undefined;
17
+ };
18
+ return walk(space.children);
19
+ }
20
+
21
+ export function findTask(space: TaskSpaceDTO, id: string): TaskDTO | undefined {
22
+ const n = findNode(space, id);
23
+ return n?.kind === 'task' ? n : undefined;
24
+ }
25
+
26
+ export function listTasks(space: TaskSpaceDTO): TaskDTO[] {
27
+ const out: TaskDTO[] = [];
28
+ const walk = (nodes: TaskTreeNode[]) => {
29
+ for (const n of nodes) (n.kind === 'task' ? out.push(n) : walk(n.children));
30
+ };
31
+ walk(space.children);
32
+ return out;
33
+ }
34
+
35
+ /** 直接子节点 id 集合(TaskStep 只能绑定它们,BP-2) */
36
+ export function directChildIds(space: TaskSpaceDTO): Set<string> {
37
+ return new Set(space.children.map((c) => c.id));
38
+ }
39
+
40
+ /**
41
+ * 节点终态判定:Task 用自身状态;Group 按 executionMode 聚合。
42
+ * 返回 undefined 表示尚未终态。
43
+ * - SEQUENTIAL / ALL:全部子节点终态才终态;全 DONE(FORWARDED 视同 DONE)→ DONE,否则 REFUSED。
44
+ * - ANY:任一 DONE/FORWARDED → DONE;全部终态且无 DONE → REFUSED。
45
+ */
46
+ export function terminalStatusOf(node: TaskTreeNode): TaskStatus | undefined {
47
+ if (node.kind === 'task') return isTerminal(node.status) ? node.status : undefined;
48
+ return aggregateGroup(node);
49
+ }
50
+
51
+ const doneLike = (s: TaskStatus) => s === 'Done' || s === 'Forwarded';
52
+
53
+ function aggregateGroup(group: TaskGroupDTO): TaskStatus | undefined {
54
+ const childStatuses = group.children.map((c) => terminalStatusOf(c));
55
+ const mode: ExecutionMode = group.executionMode;
56
+ if (mode === 'ANY') {
57
+ if (childStatuses.some((s) => s !== undefined && doneLike(s))) return 'Done';
58
+ if (childStatuses.every((s) => s !== undefined)) return 'Refused';
59
+ return undefined;
60
+ }
61
+ // SEQUENTIAL / ALL
62
+ if (childStatuses.some((s) => s === undefined)) {
63
+ // 提前失败:任一 REFUSED/ABANDONED 即整体 REFUSED(不必等其余)
64
+ if (childStatuses.some((s) => s !== undefined && !doneLike(s))) return 'Refused';
65
+ return undefined;
66
+ }
67
+ return childStatuses.every((s) => doneLike(s!)) ? 'Done' : 'Refused';
68
+ }
69
+
70
+ /** SEQUENTIAL 组内可操作性:前序兄弟(按 order)必须 done-like */
71
+ export function isOperable(space: TaskSpaceDTO, taskId: string): boolean {
72
+ const check = (nodes: TaskTreeNode[], mode: ExecutionMode | null): boolean => {
73
+ const sorted = [...nodes].sort((a, b) => a.order - b.order);
74
+ for (let i = 0; i < sorted.length; i++) {
75
+ const n = sorted[i];
76
+ if (n.id === taskId || (n.kind === 'group' && findInGroup(n, taskId))) {
77
+ if (mode === 'SEQUENTIAL') {
78
+ for (let j = 0; j < i; j++) {
79
+ const prev = terminalStatusOf(sorted[j]);
80
+ if (prev === undefined || !doneLike(prev)) return false;
81
+ }
82
+ }
83
+ return n.id === taskId ? true : check((n as TaskGroupDTO).children, (n as TaskGroupDTO).executionMode);
84
+ }
85
+ }
86
+ return false;
87
+ };
88
+ const findInGroup = (g: TaskGroupDTO, id: string): boolean => !!findNodeIn(g.children, id);
89
+ const findNodeIn = (nodes: TaskTreeNode[], id: string): TaskTreeNode | undefined => {
90
+ for (const n of nodes) {
91
+ if (n.id === id) return n;
92
+ if (n.kind === 'group') {
93
+ const hit = findNodeIn(n.children, id);
94
+ if (hit) return hit;
95
+ }
96
+ }
97
+ return undefined;
98
+ };
99
+ return check(space.children, null);
100
+ }