@mebular/fleet 0.0.0-stage → 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.
Files changed (178) hide show
  1. package/dist/agentdir.d.ts +43 -0
  2. package/dist/agentdir.d.ts.map +1 -0
  3. package/dist/agentdir.js +107 -0
  4. package/dist/agentdir.js.map +1 -0
  5. package/dist/cli.d.ts +3 -0
  6. package/dist/cli.d.ts.map +1 -0
  7. package/dist/cli.js +1135 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/collab/chatter.d.ts +54 -0
  10. package/dist/collab/chatter.d.ts.map +1 -0
  11. package/dist/collab/chatter.js +94 -0
  12. package/dist/collab/chatter.js.map +1 -0
  13. package/dist/collab/dag.d.ts +27 -0
  14. package/dist/collab/dag.d.ts.map +1 -0
  15. package/dist/collab/dag.js +107 -0
  16. package/dist/collab/dag.js.map +1 -0
  17. package/dist/collab/negotiation.d.ts +60 -0
  18. package/dist/collab/negotiation.d.ts.map +1 -0
  19. package/dist/collab/negotiation.js +90 -0
  20. package/dist/collab/negotiation.js.map +1 -0
  21. package/dist/collab/tree.d.ts +39 -0
  22. package/dist/collab/tree.d.ts.map +1 -0
  23. package/dist/collab/tree.js +177 -0
  24. package/dist/collab/tree.js.map +1 -0
  25. package/dist/config.d.ts +123 -0
  26. package/dist/config.d.ts.map +1 -0
  27. package/dist/config.js +206 -0
  28. package/dist/config.js.map +1 -0
  29. package/dist/index.d.ts +33 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +40 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/join.d.ts +50 -0
  34. package/dist/join.d.ts.map +1 -0
  35. package/dist/join.js +167 -0
  36. package/dist/join.js.map +1 -0
  37. package/dist/jointoken.d.ts +144 -0
  38. package/dist/jointoken.d.ts.map +1 -0
  39. package/dist/jointoken.js +472 -0
  40. package/dist/jointoken.js.map +1 -0
  41. package/dist/model.d.ts +64 -0
  42. package/dist/model.d.ts.map +1 -0
  43. package/dist/model.js +158 -0
  44. package/dist/model.js.map +1 -0
  45. package/dist/onboard.d.ts +90 -0
  46. package/dist/onboard.d.ts.map +1 -0
  47. package/dist/onboard.js +510 -0
  48. package/dist/onboard.js.map +1 -0
  49. package/dist/protocol/envelope.d.ts +94 -0
  50. package/dist/protocol/envelope.d.ts.map +1 -0
  51. package/dist/protocol/envelope.js +169 -0
  52. package/dist/protocol/envelope.js.map +1 -0
  53. package/dist/protocol/events.d.ts +51 -0
  54. package/dist/protocol/events.d.ts.map +1 -0
  55. package/dist/protocol/events.js +87 -0
  56. package/dist/protocol/events.js.map +1 -0
  57. package/dist/qr.d.ts +15 -0
  58. package/dist/qr.d.ts.map +1 -0
  59. package/dist/qr.js +52 -0
  60. package/dist/qr.js.map +1 -0
  61. package/dist/quickstart.d.ts +167 -0
  62. package/dist/quickstart.d.ts.map +1 -0
  63. package/dist/quickstart.js +482 -0
  64. package/dist/quickstart.js.map +1 -0
  65. package/dist/quota.d.ts +41 -0
  66. package/dist/quota.d.ts.map +1 -0
  67. package/dist/quota.js +84 -0
  68. package/dist/quota.js.map +1 -0
  69. package/dist/runtime/agent.d.ts +100 -0
  70. package/dist/runtime/agent.d.ts.map +1 -0
  71. package/dist/runtime/agent.js +260 -0
  72. package/dist/runtime/agent.js.map +1 -0
  73. package/dist/runtime/executor.d.ts +42 -0
  74. package/dist/runtime/executor.d.ts.map +1 -0
  75. package/dist/runtime/executor.js +87 -0
  76. package/dist/runtime/executor.js.map +1 -0
  77. package/dist/runtime/fairness.d.ts +25 -0
  78. package/dist/runtime/fairness.d.ts.map +1 -0
  79. package/dist/runtime/fairness.js +79 -0
  80. package/dist/runtime/fairness.js.map +1 -0
  81. package/dist/runtime/node.d.ts +91 -0
  82. package/dist/runtime/node.d.ts.map +1 -0
  83. package/dist/runtime/node.js +203 -0
  84. package/dist/runtime/node.js.map +1 -0
  85. package/dist/runtime/openchamber-http.d.ts +27 -0
  86. package/dist/runtime/openchamber-http.d.ts.map +1 -0
  87. package/dist/runtime/openchamber-http.js +118 -0
  88. package/dist/runtime/openchamber-http.js.map +1 -0
  89. package/dist/runtime/openchamber.d.ts +38 -0
  90. package/dist/runtime/openchamber.d.ts.map +1 -0
  91. package/dist/runtime/openchamber.js +43 -0
  92. package/dist/runtime/openchamber.js.map +1 -0
  93. package/dist/runtime/planner.d.ts +23 -0
  94. package/dist/runtime/planner.d.ts.map +1 -0
  95. package/dist/runtime/planner.js +17 -0
  96. package/dist/runtime/planner.js.map +1 -0
  97. package/dist/runtime/worker.d.ts +96 -0
  98. package/dist/runtime/worker.d.ts.map +1 -0
  99. package/dist/runtime/worker.js +296 -0
  100. package/dist/runtime/worker.js.map +1 -0
  101. package/dist/security.d.ts +462 -0
  102. package/dist/security.d.ts.map +1 -0
  103. package/dist/security.js +1197 -0
  104. package/dist/security.js.map +1 -0
  105. package/dist/store/daemon-store.d.ts +33 -0
  106. package/dist/store/daemon-store.d.ts.map +1 -0
  107. package/dist/store/daemon-store.js +115 -0
  108. package/dist/store/daemon-store.js.map +1 -0
  109. package/dist/store/file-store.d.ts +22 -0
  110. package/dist/store/file-store.d.ts.map +1 -0
  111. package/dist/store/file-store.js +63 -0
  112. package/dist/store/file-store.js.map +1 -0
  113. package/dist/store/mebular-store.d.ts +22 -0
  114. package/dist/store/mebular-store.d.ts.map +1 -0
  115. package/dist/store/mebular-store.js +53 -0
  116. package/dist/store/mebular-store.js.map +1 -0
  117. package/dist/store/message-store.d.ts +26 -0
  118. package/dist/store/message-store.d.ts.map +1 -0
  119. package/dist/store/message-store.js +51 -0
  120. package/dist/store/message-store.js.map +1 -0
  121. package/dist/surface.d.ts +43 -0
  122. package/dist/surface.d.ts.map +1 -0
  123. package/dist/surface.js +515 -0
  124. package/dist/surface.js.map +1 -0
  125. package/dist/transport/null.d.ts +7 -0
  126. package/dist/transport/null.d.ts.map +1 -0
  127. package/dist/transport/null.js +16 -0
  128. package/dist/transport/null.js.map +1 -0
  129. package/dist/transport/spool.d.ts +12 -0
  130. package/dist/transport/spool.d.ts.map +1 -0
  131. package/dist/transport/spool.js +54 -0
  132. package/dist/transport/spool.js.map +1 -0
  133. package/dist/transport/types.d.ts +21 -0
  134. package/dist/transport/types.d.ts.map +1 -0
  135. package/dist/transport/types.js +6 -0
  136. package/dist/transport/types.js.map +1 -0
  137. package/dist/version.json +6 -0
  138. package/package.json +37 -4
  139. package/protocol/collab.example.json +23 -0
  140. package/protocol/daemon-home.example.json +66 -0
  141. package/protocol/envelope.example.json +12 -0
  142. package/protocol/state-machine.json +14 -0
  143. package/protocol/task-tree.example.json +72 -0
  144. package/src/agentdir.ts +138 -0
  145. package/src/cli.ts +1192 -0
  146. package/src/collab/chatter.ts +116 -0
  147. package/src/collab/dag.ts +124 -0
  148. package/src/collab/negotiation.ts +130 -0
  149. package/src/collab/tree.ts +184 -0
  150. package/src/config.ts +286 -0
  151. package/src/index.ts +39 -0
  152. package/src/join.ts +227 -0
  153. package/src/jointoken.ts +609 -0
  154. package/src/model.ts +203 -0
  155. package/src/onboard.ts +601 -0
  156. package/src/protocol/envelope.ts +228 -0
  157. package/src/protocol/events.ts +135 -0
  158. package/src/qr.ts +68 -0
  159. package/src/quickstart.ts +632 -0
  160. package/src/quota.ts +112 -0
  161. package/src/runtime/agent.ts +313 -0
  162. package/src/runtime/executor.ts +114 -0
  163. package/src/runtime/fairness.ts +90 -0
  164. package/src/runtime/node.ts +250 -0
  165. package/src/runtime/openchamber-http.ts +137 -0
  166. package/src/runtime/openchamber.ts +66 -0
  167. package/src/runtime/planner.ts +36 -0
  168. package/src/runtime/worker.ts +354 -0
  169. package/src/security.ts +1660 -0
  170. package/src/store/daemon-store.ts +144 -0
  171. package/src/store/file-store.ts +75 -0
  172. package/src/store/mebular-store.ts +66 -0
  173. package/src/store/message-store.ts +62 -0
  174. package/src/surface.ts +535 -0
  175. package/src/transport/null.ts +20 -0
  176. package/src/transport/spool.ts +62 -0
  177. package/src/transport/types.ts +27 -0
  178. package/README.md +0 -3
@@ -0,0 +1,116 @@
1
+ // Fleet 协作形态 ③:配额制闲聊(M4 目标二)。
2
+ //
3
+ // 低频信息交换,受**每设备本地配额**约束(复用 `LocalQuota`):超额按策略本地拒绝/排队,
4
+ // 无全局协调。收件按 `messageId` 幂等去重,顺序无关。
5
+
6
+ import { isFleetEndpoint, type FleetEndpoint } from '../protocol/envelope.js';
7
+ import type { LocalQuota, QuotaDecision, QuotaSnapshot } from '../quota.js';
8
+ import type { Mebular } from '@mebular/core';
9
+ import { MebularMessageStore } from '../store/message-store.js';
10
+
11
+ /** 闲聊消息的节点类型(与任务事件同分区、不同类型,随既有授权同步)。 */
12
+ export const CHATTER_MESSAGE_TYPE = 'chatter_message';
13
+
14
+ /** 构造闲聊消息存储(校验 + messageId 幂等)。 */
15
+ export function chatterMessageStore(mebular: Mebular, namespace = 'tasks'): MebularMessageStore<ChatterMessage> {
16
+ return new MebularMessageStore<ChatterMessage>(mebular, {
17
+ type: CHATTER_MESSAGE_TYPE,
18
+ namespace,
19
+ validate: validateChatterMessage,
20
+ idOf: (m) => m.messageId,
21
+ });
22
+ }
23
+
24
+ /** 一条闲聊消息(幂等键 = `messageId`)。 */
25
+ export interface ChatterMessage {
26
+ v: number;
27
+ messageId: string;
28
+ from: FleetEndpoint;
29
+ topic: string;
30
+ text: string;
31
+ }
32
+
33
+ export interface ChatterValidation {
34
+ ok: boolean;
35
+ errors: string[];
36
+ }
37
+
38
+ function isNonEmptyString(value: unknown): value is string {
39
+ return typeof value === 'string' && value.length > 0;
40
+ }
41
+
42
+ export function validateChatterMessage(input: unknown): ChatterValidation {
43
+ const errors: string[] = [];
44
+ if (typeof input !== 'object' || input === null || Array.isArray(input)) {
45
+ return { ok: false, errors: ['chatter 必须是对象'] };
46
+ }
47
+ const m = input as Record<string, unknown>;
48
+ if (m.v !== 1) errors.push('v 必须为 1');
49
+ if (!isNonEmptyString(m.messageId)) errors.push('messageId 必须为非空字符串');
50
+ if (!isFleetEndpoint(m.from)) errors.push('from 必须为 {device,agent}');
51
+ if (!isNonEmptyString(m.topic)) errors.push('topic 必须为非空字符串');
52
+ if (!isNonEmptyString(m.text)) errors.push('text 必须为非空字符串');
53
+ return { ok: errors.length === 0, errors };
54
+ }
55
+
56
+ /** 本地闲聊信箱:发送走本地配额,接收幂等去重。 */
57
+ export class ChatterBox {
58
+ private readonly byId = new Map<string, ChatterMessage>();
59
+
60
+ constructor(private readonly quota: LocalQuota) {}
61
+
62
+ /** 发送:对 `from.device` 本地记账;超额按 quota 策略 accepted/queued/rejected。 */
63
+ send(message: ChatterMessage): QuotaDecision {
64
+ return this.quota.decide(message.from.device);
65
+ }
66
+
67
+ /** 接收:重复 `messageId` 返回 false(幂等)。 */
68
+ receive(message: ChatterMessage): boolean {
69
+ if (this.byId.has(message.messageId)) return false;
70
+ this.byId.set(message.messageId, message);
71
+ return true;
72
+ }
73
+
74
+ /** 收件箱(topic、messageId 字典序;确定性)。 */
75
+ inbox(): ChatterMessage[] {
76
+ return [...this.byId.values()].sort((a, b) =>
77
+ a.topic === b.topic ? (a.messageId < b.messageId ? -1 : a.messageId > b.messageId ? 1 : 0) : a.topic < b.topic ? -1 : 1,
78
+ );
79
+ }
80
+
81
+ size(): number {
82
+ return this.byId.size;
83
+ }
84
+ }
85
+
86
+ /**
87
+ * 1d-c:**live** 配额制闲聊——消息经 `MebularMessageStore`(随记忆同步)传递;发送对
88
+ * `from.device` 走**本地配额**(accepted/queued/rejected),无全局协调。收件幂等。
89
+ */
90
+ export class FleetChatter {
91
+ private readonly quota: LocalQuota;
92
+ private readonly store: MebularMessageStore<ChatterMessage>;
93
+
94
+ constructor(options: { device: string; quota: LocalQuota; store: MebularMessageStore<ChatterMessage> }) {
95
+ this.quota = options.quota;
96
+ this.store = options.store;
97
+ }
98
+
99
+ /** 发送:本地配额判定;`accepted` 才真正落图(其余按策略进本地队列/拒绝计数)。 */
100
+ async send(message: ChatterMessage): Promise<QuotaDecision> {
101
+ if (!validateChatterMessage(message).ok) return 'rejected';
102
+ const decision = this.quota.decide(message.from.device);
103
+ if (decision === 'accepted') await this.store.append(message);
104
+ return decision;
105
+ }
106
+
107
+ /** 收件箱(幂等去重;确定序)。 */
108
+ async inbox(): Promise<ChatterMessage[]> {
109
+ return this.store.all();
110
+ }
111
+
112
+ /** 本地账本快照(按 device 字典序;用于账本守恒断言)。 */
113
+ ledger(): QuotaSnapshot[] {
114
+ return this.quota.snapshot();
115
+ }
116
+ }
@@ -0,0 +1,124 @@
1
+ // Fleet 协作形态 ①:审查 DAG(M4 目标二)。
2
+ //
3
+ // 任务派生为有向无环图(计划→分派→审查→汇总),父子关系由 `trace.causedBy` 表达。本模块只做
4
+ // **纯模型**:由任务状态推导边、禁环(检测 + 创建守卫)、完成判定(从 root 可达的全部节点终态)。
5
+ // 不引入调度语义、不触碰 core。
6
+
7
+ import type { TaskState } from '../model.js';
8
+
9
+ /** DAG 边:parent → child(child 由 parent 派生)。 */
10
+ export interface DagEdge {
11
+ parent: string;
12
+ child: string;
13
+ }
14
+
15
+ /** 由任务状态推导父子边(`child.trace.causedBy → parent`),确定序。 */
16
+ export function deriveEdges(states: readonly TaskState[]): DagEdge[] {
17
+ const edges: DagEdge[] = [];
18
+ for (const state of states) {
19
+ const parent = state.trace.causedBy;
20
+ if (parent !== undefined && parent !== state.taskId) edges.push({ parent, child: state.taskId });
21
+ }
22
+ edges.sort((a, b) =>
23
+ a.parent === b.parent ? (a.child < b.child ? -1 : a.child > b.child ? 1 : 0) : a.parent < b.parent ? -1 : 1,
24
+ );
25
+ return edges;
26
+ }
27
+
28
+ /** 检测有向环;返回一条环路径(首尾同节点)或 null。确定序(按节点/邻接字典序 DFS)。 */
29
+ export function detectCycle(edges: readonly DagEdge[]): string[] | null {
30
+ const children = new Map<string, string[]>();
31
+ const nodes = new Set<string>();
32
+ for (const edge of edges) {
33
+ nodes.add(edge.parent);
34
+ nodes.add(edge.child);
35
+ const list = children.get(edge.parent);
36
+ if (list) list.push(edge.child);
37
+ else children.set(edge.parent, [edge.child]);
38
+ }
39
+ for (const list of children.values()) list.sort();
40
+
41
+ const stateOf = new Map<string, 0 | 1 | 2>();
42
+ const stack: string[] = [];
43
+ let cycle: string[] | null = null;
44
+ const visit = (node: string): boolean => {
45
+ stateOf.set(node, 1);
46
+ stack.push(node);
47
+ for (const child of children.get(node) ?? []) {
48
+ const st = stateOf.get(child) ?? 0;
49
+ if (st === 1) {
50
+ const start = stack.indexOf(child);
51
+ cycle = [...stack.slice(start), child];
52
+ return true;
53
+ }
54
+ if (st === 0 && visit(child)) return true;
55
+ }
56
+ stateOf.set(node, 2);
57
+ stack.pop();
58
+ return false;
59
+ };
60
+ for (const node of [...nodes].sort()) {
61
+ if ((stateOf.get(node) ?? 0) === 0 && visit(node)) break;
62
+ }
63
+ return cycle;
64
+ }
65
+
66
+ /** 新增 `parent → child` 是否会成环(`parent === child`,或 child 已是 parent 的祖先)。 */
67
+ export function wouldCreateCycle(edges: readonly DagEdge[], parent: string, child: string): boolean {
68
+ if (parent === child) return true;
69
+ const parentOf = new Map<string, string>();
70
+ for (const edge of edges) parentOf.set(edge.child, edge.parent);
71
+ let current: string | undefined = parent;
72
+ const seen = new Set<string>();
73
+ while (current !== undefined && !seen.has(current)) {
74
+ if (current === child) return true;
75
+ seen.add(current);
76
+ current = parentOf.get(current);
77
+ }
78
+ return false;
79
+ }
80
+
81
+ /** 创建子任务的守卫:成环则抛错(负例测试)。 */
82
+ export function assertAcyclicParent(edges: readonly DagEdge[], parent: string, child: string): void {
83
+ if (wouldCreateCycle(edges, parent, child)) {
84
+ throw new Error(`DAG 成环:${parent} → ${child}`);
85
+ }
86
+ }
87
+
88
+ /** 完成判定结果。 */
89
+ export interface DagCompletion {
90
+ root: string;
91
+ /** 从 root 可达的全部节点(含 root),字典序 */
92
+ reachable: string[];
93
+ /** 其中尚未终态的节点,字典序 */
94
+ pending: string[];
95
+ /** 全部可达节点终态 = 完成(即所有子任务终态且 root 已汇总为终态) */
96
+ complete: boolean;
97
+ }
98
+
99
+ /** 从 `root` 可达子图的完成判定。 */
100
+ export function dagCompletion(
101
+ edges: readonly DagEdge[],
102
+ isTerminal: (id: string) => boolean,
103
+ root: string,
104
+ ): DagCompletion {
105
+ const children = new Map<string, string[]>();
106
+ for (const edge of edges) {
107
+ const list = children.get(edge.parent);
108
+ if (list) list.push(edge.child);
109
+ else children.set(edge.parent, [edge.child]);
110
+ }
111
+ for (const list of children.values()) list.sort();
112
+
113
+ const reachable = new Set<string>();
114
+ const stack = [root];
115
+ while (stack.length > 0) {
116
+ const node = stack.pop()!;
117
+ if (reachable.has(node)) continue;
118
+ reachable.add(node);
119
+ for (const child of children.get(node) ?? []) stack.push(child);
120
+ }
121
+ const sorted = [...reachable].sort();
122
+ const pending = sorted.filter((id) => !isTerminal(id));
123
+ return { root, reachable: sorted, pending, complete: pending.length === 0 };
124
+ }
@@ -0,0 +1,130 @@
1
+ // Fleet 协作形态 ②:有限协商(M4 目标二)。
2
+ //
3
+ // 任务可回写「需澄清 / 反提案 / 接受 / 拒绝」,但**步数有上限**(可配);超限 → 任务 `failed` 带原因。
4
+ // 协商消息独立于 `TaskEvent`(**不改线格式**);跟踪器对消息**幂等去重**且结果**与到达顺序无关**。
5
+
6
+ import type { FleetEndpoint } from '../protocol/envelope.js';
7
+ import { isFleetEndpoint } from '../protocol/envelope.js';
8
+ import type { Mebular } from '@mebular/core';
9
+ import { MebularMessageStore } from '../store/message-store.js';
10
+
11
+ /** 协商消息的节点类型(与任务事件同分区、不同类型,随既有授权同步)。 */
12
+ export const NEGOTIATION_MESSAGE_TYPE = 'negotiation_message';
13
+
14
+ /** 构造协商消息存储(校验 + messageId 幂等)。 */
15
+ export function negotiationMessageStore(
16
+ mebular: Mebular,
17
+ namespace = 'tasks',
18
+ ): MebularMessageStore<NegotiationMessage> {
19
+ return new MebularMessageStore<NegotiationMessage>(mebular, {
20
+ type: NEGOTIATION_MESSAGE_TYPE,
21
+ namespace,
22
+ validate: validateNegotiationMessage,
23
+ idOf: (m) => m.messageId,
24
+ });
25
+ }
26
+
27
+ /** 协商消息类型。 */
28
+ export const NEGOTIATION_KINDS = ['clarify', 'counter', 'accept', 'reject'] as const;
29
+ export type NegotiationKind = (typeof NEGOTIATION_KINDS)[number];
30
+
31
+ /** 一条协商消息(幂等键 = `messageId`)。 */
32
+ export interface NegotiationMessage {
33
+ v: number;
34
+ messageId: string;
35
+ taskId: string;
36
+ /** 第几轮(由发起方给;确定性上取「最大轮」) */
37
+ round: number;
38
+ from: FleetEndpoint;
39
+ kind: NegotiationKind;
40
+ text?: string;
41
+ }
42
+
43
+ export interface NegotiationValidation {
44
+ ok: boolean;
45
+ errors: string[];
46
+ }
47
+
48
+ function isNonEmptyString(value: unknown): value is string {
49
+ return typeof value === 'string' && value.length > 0;
50
+ }
51
+
52
+ export function validateNegotiationMessage(input: unknown): NegotiationValidation {
53
+ const errors: string[] = [];
54
+ if (typeof input !== 'object' || input === null || Array.isArray(input)) {
55
+ return { ok: false, errors: ['negotiation 必须是对象'] };
56
+ }
57
+ const m = input as Record<string, unknown>;
58
+ if (m.v !== 1) errors.push('v 必须为 1');
59
+ if (!isNonEmptyString(m.messageId)) errors.push('messageId 必须为非空字符串');
60
+ if (!isNonEmptyString(m.taskId)) errors.push('taskId 必须为非空字符串');
61
+ if (typeof m.round !== 'number' || !Number.isInteger(m.round) || m.round < 0) errors.push('round 必须为非负整数');
62
+ if (!isFleetEndpoint(m.from)) errors.push('from 必须为 {device,agent}');
63
+ if (!isNonEmptyString(m.kind) || !(NEGOTIATION_KINDS as readonly string[]).includes(m.kind)) {
64
+ errors.push(`kind 必须为 ${NEGOTIATION_KINDS.join('|')}`);
65
+ }
66
+ if (m.text !== undefined && !isNonEmptyString(m.text)) errors.push('text 若存在须为非空字符串');
67
+ return { ok: errors.length === 0, errors };
68
+ }
69
+
70
+ /** 跟踪结果(与到达顺序无关)。 */
71
+ export interface NegotiationStatus {
72
+ rounds: number;
73
+ maxRounds: number;
74
+ accepted: boolean;
75
+ rejected: boolean;
76
+ exceeded: boolean;
77
+ }
78
+
79
+ /** 有限协商裁定。 */
80
+ export type NegotiationDecision =
81
+ | { action: 'proceed' }
82
+ | { action: 'continue' }
83
+ | { action: 'fail'; reason: string };
84
+
85
+ /** 协商跟踪器:幂等去重 + 顺序无关。 */
86
+ export class NegotiationTracker {
87
+ readonly taskId: string;
88
+ private readonly maxRounds: number;
89
+ private readonly byId = new Map<string, NegotiationMessage>();
90
+
91
+ constructor(taskId: string, options: { maxRounds: number }) {
92
+ if (!Number.isInteger(options.maxRounds) || options.maxRounds < 0) {
93
+ throw new Error(`maxRounds 必须为非负整数:${options.maxRounds}`);
94
+ }
95
+ this.taskId = taskId;
96
+ this.maxRounds = options.maxRounds;
97
+ }
98
+
99
+ /** 应用一条消息;重复 `messageId` 返回 false(幂等)。 */
100
+ apply(message: NegotiationMessage): boolean {
101
+ if (message.taskId !== this.taskId) throw new Error(`协商消息 taskId 不符:${message.taskId}`);
102
+ if (this.byId.has(message.messageId)) return false;
103
+ this.byId.set(message.messageId, message);
104
+ return true;
105
+ }
106
+
107
+ /** 消息列表(轮次、messageId 字典序)。 */
108
+ messages(): NegotiationMessage[] {
109
+ return [...this.byId.values()].sort((a, b) =>
110
+ a.round === b.round ? (a.messageId < b.messageId ? -1 : a.messageId > b.messageId ? 1 : 0) : a.round - b.round,
111
+ );
112
+ }
113
+
114
+ status(): NegotiationStatus {
115
+ const messages = this.messages();
116
+ const rounds = messages.reduce((n, m) => Math.max(n, m.round), 0);
117
+ const accepted = messages.some((m) => m.kind === 'accept');
118
+ const rejected = messages.some((m) => m.kind === 'reject');
119
+ const exceeded = !accepted && rounds > this.maxRounds;
120
+ return { rounds, maxRounds: this.maxRounds, accepted, rejected, exceeded };
121
+ }
122
+ }
123
+
124
+ /** 由状态裁定:接受→proceed;拒绝→fail;超限→fail(带原因);否则 continue。 */
125
+ export function negotiationDecision(status: NegotiationStatus): NegotiationDecision {
126
+ if (status.accepted) return { action: 'proceed' };
127
+ if (status.rejected) return { action: 'fail', reason: 'NEGOTIATION_REJECTED' };
128
+ if (status.exceeded) return { action: 'fail', reason: `NEGOTIATION_LIMIT: ${status.rounds}>${status.maxRounds}` };
129
+ return { action: 'continue' };
130
+ }
@@ -0,0 +1,184 @@
1
+ // W1 任务树:**预算随树单调递减** + 链长/派发策略不变式 + 确定性摊派。
2
+ //
3
+ // 纯函数、无墙钟:同一批 `TaskState` 在任何端得到同一判定(全端一致)。
4
+ // 入口(node/worker 接收 `created`)与 reducer 不变式共用 `admitCreated` / `treeViolations`。
5
+
6
+ import type { TaskState } from '../model.js';
7
+ import type { TaskEvent } from '../protocol/events.js';
8
+ import { DEFAULT_ROOT_BUDGET, type TaskBudget, type TaskDispatch } from '../protocol/envelope.js';
9
+
10
+ export type TreeViolationReason =
11
+ | 'BUDGET_EXCEEDED'
12
+ | 'CHAIN_TOO_DEEP'
13
+ | 'TOO_MANY_CHILDREN'
14
+ | 'TOO_MANY_TASKS'
15
+ | 'ROOT_ONLY';
16
+
17
+ export interface TreeViolation {
18
+ taskId: string;
19
+ reason: TreeViolationReason;
20
+ detail?: string;
21
+ }
22
+
23
+ /** 父任务的“剩余预算”:每下一层 `maxDepth`/`maxTasks` 各减一(越深越小)。 */
24
+ export function remainingBudget(parent: TaskBudget): TaskBudget {
25
+ return {
26
+ maxDepth: Math.max(0, parent.maxDepth - 1),
27
+ maxChildren: parent.maxChildren,
28
+ maxTasks: Math.max(0, parent.maxTasks - 1),
29
+ };
30
+ }
31
+
32
+ /** 子任务**实际生效**预算:声明值优先;未声明则按父剩余推导;root 无声明用默认。 */
33
+ export function effectiveBudget(state: TaskState, byId: ReadonlyMap<string, TaskState>, seen = new Set<string>()): TaskBudget {
34
+ if (state.budget !== undefined) return state.budget;
35
+ const parentId = state.trace.causedBy;
36
+ if (parentId === undefined) return DEFAULT_ROOT_BUDGET;
37
+ const parent = byId.get(parentId);
38
+ if (parent === undefined || seen.has(parentId)) return DEFAULT_ROOT_BUDGET;
39
+ seen.add(state.taskId);
40
+ return remainingBudget(effectiveBudget(parent, byId, seen));
41
+ }
42
+
43
+ /** 深度(祖先数):root=0;按 `causedBy` 递归(带环保护)。 */
44
+ export function depthOf(state: TaskState, byId: ReadonlyMap<string, TaskState>, seen = new Set<string>()): number {
45
+ const parentId = state.trace.causedBy;
46
+ if (parentId === undefined) return 0;
47
+ if (seen.has(state.taskId)) return 0;
48
+ const parent = byId.get(parentId);
49
+ if (parent === undefined) return 1; // 父不在集合(尚未到达):按一层看待
50
+ seen.add(state.taskId);
51
+ return 1 + depthOf(parent, byId, seen);
52
+ }
53
+
54
+ /** root 派发策略:沿 `causedBy` 找到 root 的 `dispatch`(缺省 `children-ok`)。 */
55
+ export function rootDispatch(state: TaskState, byId: ReadonlyMap<string, TaskState>): TaskDispatch {
56
+ let current: TaskState | undefined = state;
57
+ const seen = new Set<string>();
58
+ while (current !== undefined && !seen.has(current.taskId)) {
59
+ seen.add(current.taskId);
60
+ if (current.trace.causedBy === undefined) return current.dispatch ?? 'children-ok';
61
+ current = byId.get(current.trace.causedBy);
62
+ }
63
+ return 'children-ok';
64
+ }
65
+
66
+ function subtreeSize(rootId: string, childrenOf: ReadonlyMap<string, string[]>): number {
67
+ let n = 0;
68
+ const stack = [rootId];
69
+ const seen = new Set<string>();
70
+ while (stack.length > 0) {
71
+ const id = stack.pop()!;
72
+ if (seen.has(id)) continue;
73
+ seen.add(id);
74
+ n += 1;
75
+ for (const child of childrenOf.get(id) ?? []) stack.push(child);
76
+ }
77
+ return n;
78
+ }
79
+
80
+ /** 全树不变式:返回违规列表(按 taskId 确定序;空 = 全部合法)。 */
81
+ export function treeViolations(states: readonly TaskState[]): TreeViolation[] {
82
+ const byId = new Map(states.map((s) => [s.taskId, s]));
83
+ const childrenOf = new Map<string, string[]>();
84
+ for (const s of states) {
85
+ const parent = s.trace.causedBy;
86
+ if (parent === undefined) continue;
87
+ const list = childrenOf.get(parent);
88
+ if (list) list.push(s.taskId);
89
+ else childrenOf.set(parent, [s.taskId]);
90
+ }
91
+ for (const list of childrenOf.values()) list.sort();
92
+
93
+ const violations: TreeViolation[] = [];
94
+ for (const state of [...states].sort((a, b) => (a.taskId < b.taskId ? -1 : 1))) {
95
+ const parentId = state.trace.causedBy;
96
+ if (parentId === undefined) continue; // root:默认预算已上界,无需逐条判定
97
+ const parent = byId.get(parentId);
98
+ if (parent === undefined) continue; // 父未知:无法判定(到达后重估)
99
+ const parentEff = effectiveBudget(parent, byId);
100
+ const remain = remainingBudget(parentEff);
101
+ const childEff = effectiveBudget(state, byId);
102
+ if (childEff.maxDepth > remain.maxDepth || childEff.maxTasks > remain.maxTasks || childEff.maxChildren > remain.maxChildren) {
103
+ violations.push({ taskId: state.taskId, reason: 'BUDGET_EXCEEDED', detail: `${JSON.stringify(childEff)} > ${JSON.stringify(remain)}` });
104
+ }
105
+ const rootState = rootOf(state, byId);
106
+ const rootEff = rootState !== undefined ? effectiveBudget(rootState, byId) : DEFAULT_ROOT_BUDGET;
107
+ if (depthOf(state, byId) > rootEff.maxDepth) {
108
+ violations.push({ taskId: state.taskId, reason: 'CHAIN_TOO_DEEP', detail: `depth=${depthOf(state, byId)} > ${rootEff.maxDepth}` });
109
+ }
110
+ if (rootDispatch(state, byId) === 'root-only') {
111
+ violations.push({ taskId: state.taskId, reason: 'ROOT_ONLY', detail: 'dispatch=root-only' });
112
+ }
113
+ const direct = childrenOf.get(parentId)?.length ?? 0;
114
+ if (direct > parentEff.maxChildren) {
115
+ violations.push({ taskId: parentId, reason: 'TOO_MANY_CHILDREN', detail: `${direct} > ${parentEff.maxChildren}` });
116
+ }
117
+ }
118
+ // 子树总量:对每个 root 检查
119
+ for (const s of states) {
120
+ if (s.trace.causedBy !== undefined) continue;
121
+ const eff = effectiveBudget(s, byId);
122
+ const size = subtreeSize(s.taskId, childrenOf);
123
+ if (size > eff.maxTasks) violations.push({ taskId: s.taskId, reason: 'TOO_MANY_TASKS', detail: `${size} > ${eff.maxTasks}` });
124
+ }
125
+ // 去重(同一 taskId+reason)
126
+ const seen = new Set<string>();
127
+ return violations.filter((v) => {
128
+ const key = `${v.taskId}|${v.reason}`;
129
+ if (seen.has(key)) return false;
130
+ seen.add(key);
131
+ return true;
132
+ });
133
+ }
134
+
135
+ function rootOf(state: TaskState, byId: ReadonlyMap<string, TaskState>): TaskState | undefined {
136
+ let current: TaskState | undefined = state;
137
+ const seen = new Set<string>();
138
+ while (current !== undefined && !seen.has(current.taskId)) {
139
+ seen.add(current.taskId);
140
+ if (current.trace.causedBy === undefined) return current;
141
+ current = byId.get(current.trace.causedBy);
142
+ }
143
+ return undefined;
144
+ }
145
+
146
+ /**
147
+ * 权威视图的**可采纳任务集**:剔除违反树不变式的任务(越预算/越链长/root-only…)。
148
+ * node/worker 的 `states()` 用它过滤,使经**记忆同步**到达的无效 `created` 也不进入权威视图
149
+ * (与传输入口拒收共用同一纯函数,全端一致)。
150
+ */
151
+ export function admissibleTaskIds(states: readonly TaskState[]): Set<string> {
152
+ const bad = new Set(treeViolations(states).map((v) => v.taskId));
153
+ return new Set(states.map((s) => s.taskId).filter((id) => !bad.has(id)));
154
+ }
155
+
156
+ /**
157
+ * 入口准入:把一个候选事件并入后,树不变式是否仍成立(仅 `created` 需判定)。
158
+ * 返回 `{ok:false, reason}` 时,调用方**不得入库**(无效事件)。
159
+ */
160
+ export function admitCreated(
161
+ event: TaskEvent,
162
+ states: readonly TaskState[],
163
+ reduceOne: (events: TaskEvent[]) => TaskState | null,
164
+ ): { ok: true } | { ok: false; reason: TreeViolationReason; detail?: string } {
165
+ if (event.type !== 'created') return { ok: true };
166
+ const state = reduceOne([event]);
167
+ if (state === null) return { ok: false, reason: 'CHAIN_TOO_DEEP', detail: 'created 事件无法归约' };
168
+ const violations = treeViolations([...states, state]).filter((v) => v.taskId === event.taskId);
169
+ if (violations.length === 0) return { ok: true };
170
+ const first = violations[0]!;
171
+ return { ok: false, reason: first.reason, ...(first.detail !== undefined ? { detail: first.detail } : {}) };
172
+ }
173
+
174
+ /** 确定性摊派:等价目标间按 `hash(taskId) mod N`(稳定字典序)。 */
175
+ export function deterministicTarget(taskId: string, targets: readonly string[]): string | null {
176
+ if (targets.length === 0) return null;
177
+ const sorted = [...targets].sort();
178
+ let h = 2166136261;
179
+ for (let i = 0; i < taskId.length; i++) {
180
+ h ^= taskId.charCodeAt(i);
181
+ h = Math.imul(h, 16777619);
182
+ }
183
+ return sorted[Math.abs(h) % sorted.length]!;
184
+ }