@dsh-workbench/shared 1.1.8 → 1.1.10

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 (226) hide show
  1. package/dist/api/blocker-table.d.ts +1 -0
  2. package/dist/api/blocker-table.js +1 -1
  3. package/dist/components/SelectField.css +0 -6
  4. package/dist/components/SelectField.d.ts +2 -3
  5. package/dist/components/SelectField.js +9 -30
  6. package/dist/components/SelectFieldNodes.d.ts +3 -1
  7. package/dist/components/SelectFieldNodes.js +2 -1
  8. package/dist/components/SelectFieldWheel.d.ts +9 -0
  9. package/dist/components/SelectFieldWheel.js +35 -0
  10. package/dist/config/constants.d.ts +4 -4
  11. package/dist/config/constants.js +4 -4
  12. package/dist/contract/capabilities.d.ts +29 -0
  13. package/dist/contract/capabilities.js +1 -0
  14. package/dist/contract/needs.d.ts +3 -3
  15. package/dist/contract/needs.js +1 -1
  16. package/dist/contract/plugin-source.d.ts +19 -0
  17. package/dist/contract/protocol/bundle-install.d.ts +3 -1
  18. package/dist/contract/protocol/bundle-install.js +3 -0
  19. package/dist/contract/protocol/market-index.d.ts +1 -1
  20. package/dist/contract/protocol/platform.d.ts +1 -1
  21. package/dist/contract/runner.d.ts +6 -0
  22. package/dist/contract/runner.js +1 -0
  23. package/dist/contract/unit.d.ts +4 -4
  24. package/dist/host/adapter-contract.d.ts +3 -0
  25. package/dist/host/adapter-contract.js +93 -0
  26. package/dist/host/adapter-set.js +6 -4
  27. package/dist/host/adapter-types.d.ts +1 -1
  28. package/dist/host/bundle-manager.d.ts +3 -0
  29. package/dist/host/bundle-manager.js +181 -0
  30. package/dist/host/cli.js +3 -1
  31. package/dist/host/config-sync.d.ts +48 -0
  32. package/dist/host/config-sync.js +342 -0
  33. package/dist/host/dsh-plugins.d.ts +5 -0
  34. package/dist/host/dsh-plugins.js +51 -0
  35. package/dist/host/extension.js +1 -1
  36. package/dist/host/flow/check.d.ts +9 -0
  37. package/dist/host/flow/check.js +179 -0
  38. package/dist/host/flow/expr.d.ts +30 -0
  39. package/dist/host/flow/expr.js +99 -0
  40. package/dist/host/flow/kit.d.ts +14 -0
  41. package/dist/host/flow/kit.js +13 -0
  42. package/dist/host/flow/run.d.ts +13 -0
  43. package/dist/host/flow/run.js +287 -0
  44. package/dist/host/flow/types.d.ts +188 -0
  45. package/dist/host/flow/types.js +1 -0
  46. package/dist/host/host-actions.d.ts +2 -0
  47. package/dist/host/host-actions.js +5 -0
  48. package/dist/host/host-kit.d.ts +19 -0
  49. package/dist/host/host-kit.js +14 -0
  50. package/dist/host/http.d.ts +2 -6
  51. package/dist/host/http.js +2 -2
  52. package/dist/host/installed-module.d.ts +23 -0
  53. package/dist/host/installed-module.js +264 -0
  54. package/dist/host/lib-install.d.ts +7 -9
  55. package/dist/host/lib-install.js +9 -12
  56. package/dist/host/lib-runners.d.ts +3 -5
  57. package/dist/host/lib-runners.js +2 -2
  58. package/dist/host/local-declaration.d.ts +7 -0
  59. package/dist/host/local-declaration.js +33 -0
  60. package/dist/host/module-root.d.ts +1 -0
  61. package/dist/host/module-root.js +5 -0
  62. package/dist/host/npm-registry.d.ts +10 -0
  63. package/dist/host/npm-registry.js +16 -0
  64. package/dist/host/opencli-runner.d.ts +2 -1
  65. package/dist/host/package-content.d.ts +7 -0
  66. package/dist/host/package-content.js +69 -0
  67. package/dist/host/package-kind.d.ts +13 -0
  68. package/dist/host/package-kind.js +51 -0
  69. package/dist/host/package-route.d.ts +31 -0
  70. package/dist/host/package-route.js +57 -0
  71. package/dist/host/paths.d.ts +11 -19
  72. package/dist/host/paths.js +16 -43
  73. package/dist/host/platform.js +1 -1
  74. package/dist/host/plugin-ledger.d.ts +8 -8
  75. package/dist/host/plugin-ledger.js +23 -16
  76. package/dist/host/plugin.d.ts +1 -2
  77. package/dist/host/plugin.js +4 -5
  78. package/dist/host/port-types.d.ts +42 -9
  79. package/dist/host/preflight.d.ts +3 -2
  80. package/dist/host/profile-create.d.ts +18 -0
  81. package/dist/host/profile-create.js +118 -0
  82. package/dist/host/profile-registries.d.ts +10 -0
  83. package/dist/host/profile-registries.js +150 -0
  84. package/dist/host/registry-declaration.d.ts +21 -0
  85. package/dist/host/registry-declaration.js +33 -0
  86. package/dist/host/release-probe.d.ts +34 -0
  87. package/dist/host/release-probe.js +88 -0
  88. package/dist/host/runner.d.ts +26 -0
  89. package/dist/host/runner.js +50 -0
  90. package/dist/host/shell.d.ts +0 -2
  91. package/dist/host/shell.js +0 -7
  92. package/dist/host/spec.js +1 -1
  93. package/dist/host/supply-index.d.ts +1 -0
  94. package/dist/host/supply-index.js +8 -7
  95. package/dist/host/supply-port.js +13 -3
  96. package/dist/host/unit-face.d.ts +2 -1
  97. package/dist/host/version.js +11 -6
  98. package/dist/host.d.ts +24 -6
  99. package/dist/host.js +23 -6
  100. package/dist/i18n/en.js +23 -24
  101. package/dist/i18n/index.d.ts +23 -24
  102. package/dist/i18n/runtime-terms.d.ts +15 -0
  103. package/dist/i18n/runtime-terms.js +22 -0
  104. package/dist/i18n/zh.d.ts +22 -23
  105. package/dist/i18n/zh.js +22 -23
  106. package/dist/index.d.ts +4 -0
  107. package/dist/index.js +4 -0
  108. package/dist/testing/fake-adapter-set.d.ts +5 -0
  109. package/dist/testing/fake-adapter-set.js +150 -0
  110. package/dist/testing/needs.js +1 -1
  111. package/dist/testing.d.ts +1 -0
  112. package/dist/testing.js +1 -0
  113. package/package.json +1 -1
  114. package/src/api/__tests__/blocker-table.test.ts +25 -0
  115. package/src/api/{workbench-client.test.ts → __tests__/workbench-client.test.ts} +2 -2
  116. package/src/api/blocker-table.ts +6 -2
  117. package/src/components/SelectField.css +0 -6
  118. package/src/components/SelectField.tsx +7 -36
  119. package/src/components/SelectFieldNodes.tsx +4 -1
  120. package/src/components/{Card.test.tsx → __tests__/Card.test.tsx} +2 -2
  121. package/src/components/{CardAnchor.test.tsx → __tests__/CardAnchor.test.tsx} +3 -3
  122. package/src/components/{CardHead.test.tsx → __tests__/CardHead.test.tsx} +1 -1
  123. package/src/components/{ChoiceGroup.test.tsx → __tests__/ChoiceGroup.test.tsx} +2 -2
  124. package/src/components/{ConfirmBar.test.tsx → __tests__/ConfirmBar.test.tsx} +5 -5
  125. package/src/components/{Field.test.tsx → __tests__/Field.test.tsx} +1 -1
  126. package/src/components/{Form.test.tsx → __tests__/Form.test.tsx} +5 -5
  127. package/src/components/{FormActions.test.tsx → __tests__/FormActions.test.tsx} +3 -3
  128. package/src/components/{FormRows.test.tsx → __tests__/FormRows.test.tsx} +2 -2
  129. package/src/components/{FormTypes.test.ts → __tests__/FormTypes.test.ts} +1 -1
  130. package/src/components/{FormWiring.test.tsx → __tests__/FormWiring.test.tsx} +4 -4
  131. package/src/components/{Link.test.tsx → __tests__/Link.test.tsx} +1 -1
  132. package/src/components/{Rows.test.tsx → __tests__/Rows.test.tsx} +2 -2
  133. package/src/components/{SelectField.test.tsx → __tests__/SelectField.test.tsx} +7 -37
  134. package/src/components/{Spinner.test.tsx → __tests__/Spinner.test.tsx} +4 -4
  135. package/src/config/{constants.test.ts → __tests__/constants.test.ts} +1 -1
  136. package/src/config/{storage.test.ts → __tests__/storage.test.ts} +1 -1
  137. package/src/config/constants.ts +4 -4
  138. package/src/contract/capabilities.ts +30 -0
  139. package/src/contract/needs.ts +4 -4
  140. package/src/contract/plugin-source.ts +24 -0
  141. package/src/contract/protocol/bundle-install.ts +7 -1
  142. package/src/contract/protocol/market-index.ts +1 -0
  143. package/src/contract/protocol/platform.ts +1 -1
  144. package/src/contract/runner.ts +6 -0
  145. package/src/contract/unit.ts +4 -4
  146. package/src/host/{extension.test.ts → __tests__/extension.test.ts} +4 -4
  147. package/src/host/{install-failure.test.ts → __tests__/install-failure.test.ts} +1 -1
  148. package/src/host/{installed-sources.test.ts → __tests__/installed-sources.test.ts} +1 -1
  149. package/src/host/__tests__/lib-runners.test.ts +11 -2
  150. package/src/host/__tests__/plugin-ledger.test.ts +19 -6
  151. package/src/host/{recipe.test.ts → __tests__/recipe.test.ts} +3 -3
  152. package/src/host/__tests__/runtime-layout.test.ts +14 -0
  153. package/src/host/__tests__/supply-index.test.ts +2 -2
  154. package/src/host/__tests__/supply-port.test.ts +32 -0
  155. package/src/host/adapter-contract.ts +93 -0
  156. package/src/host/adapter-set.ts +6 -4
  157. package/src/host/adapter-types.ts +1 -1
  158. package/src/host/bundle-manager.ts +195 -0
  159. package/src/host/cli.ts +3 -1
  160. package/src/host/config-sync.ts +386 -0
  161. package/src/host/dsh-plugins.ts +60 -0
  162. package/src/host/extension.ts +1 -1
  163. package/src/host/flow/__tests__/advanced.test.ts +302 -0
  164. package/src/host/flow/__tests__/check.test.ts +169 -0
  165. package/src/host/flow/__tests__/run.test.ts +301 -0
  166. package/src/host/flow/check.ts +216 -0
  167. package/src/host/flow/expr.ts +94 -0
  168. package/src/host/flow/kit.ts +30 -0
  169. package/src/host/flow/run.ts +325 -0
  170. package/src/host/flow/types.ts +199 -0
  171. package/src/host/host-actions.ts +7 -0
  172. package/src/host/host-kit.ts +26 -0
  173. package/src/host/http.ts +6 -8
  174. package/src/host/installed-module.ts +272 -0
  175. package/src/host/lib-install.ts +14 -12
  176. package/src/host/lib-runners.ts +6 -8
  177. package/src/host/local-declaration.ts +37 -0
  178. package/src/host/module-root.ts +6 -0
  179. package/src/host/npm-registry.ts +21 -0
  180. package/src/host/package-content.ts +76 -0
  181. package/src/host/package-kind.ts +63 -0
  182. package/src/host/package-route.ts +93 -0
  183. package/src/host/paths.ts +27 -52
  184. package/src/host/platform.ts +1 -1
  185. package/src/host/plugin-ledger.ts +25 -18
  186. package/src/host/plugin.ts +4 -5
  187. package/src/host/port-types.ts +43 -9
  188. package/src/host/preflight.ts +3 -2
  189. package/src/host/profile-create.ts +133 -0
  190. package/src/host/profile-registries.ts +145 -0
  191. package/src/host/registry-declaration.ts +70 -0
  192. package/src/host/release-probe.ts +202 -0
  193. package/src/host/shell.ts +0 -9
  194. package/src/host/spec.ts +1 -1
  195. package/src/host/supply-index.ts +9 -7
  196. package/src/host/supply-port.ts +13 -3
  197. package/src/host/unit-face.ts +2 -1
  198. package/src/host/version.ts +12 -7
  199. package/src/host.ts +34 -6
  200. package/src/i18n/{index.test.ts → __tests__/index.test.ts} +3 -3
  201. package/src/i18n/{merge.test.ts → __tests__/merge.test.ts} +1 -1
  202. package/src/i18n/{zh.test.ts → __tests__/zh.test.ts} +2 -2
  203. package/src/i18n/en.ts +23 -25
  204. package/src/i18n/runtime-terms.ts +26 -0
  205. package/src/i18n/zh.ts +22 -24
  206. package/src/index.ts +4 -0
  207. package/src/lib/{board-fact.test.ts → __tests__/board-fact.test.ts} +1 -1
  208. package/src/lib/{busy.test.ts → __tests__/busy.test.ts} +1 -1
  209. package/src/lib/{form-problem.test.ts → __tests__/form-problem.test.ts} +1 -1
  210. package/src/lib/{git-address.test.ts → __tests__/git-address.test.ts} +1 -1
  211. package/src/lib/{react.test.ts → __tests__/react.test.ts} +1 -1
  212. package/src/lib/{text.test.ts → __tests__/text.test.ts} +1 -1
  213. package/src/lib/{ui.test.ts → __tests__/ui.test.ts} +1 -1
  214. package/src/testing/__tests__/fake-adapter-set.test.ts +28 -0
  215. package/src/testing/fake-adapter-set.ts +173 -0
  216. package/src/testing/needs.ts +1 -1
  217. package/src/testing.ts +1 -0
  218. package/src/host/__tests__/command-name.test.ts +0 -16
  219. package/src/host/__tests__/opencli-runner.test.ts +0 -120
  220. package/src/host/channel-plugin.ts +0 -55
  221. package/src/host/command-name.ts +0 -15
  222. package/src/host/opencli-runner.ts +0 -76
  223. package/src/host/paths.test.ts +0 -26
  224. package/src/host/plugin-source.ts +0 -137
  225. package/src/host/runtime-links.test.ts +0 -31
  226. package/src/host/runtime-links.ts +0 -23
@@ -0,0 +1,301 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { createFlowActionRegistry, runFlow } from '../run.js';
3
+ import type {
4
+ FlowAction,
5
+ FlowActionRegistry,
6
+ FlowDefinition,
7
+ FlowResult,
8
+ FlowStepEvent,
9
+ FlowValue,
10
+ } from '../types.js';
11
+
12
+ const ref = (path: string): FlowValue => ({ $ref: path });
13
+ const lit = (value: unknown): FlowValue => ({ $lit: value });
14
+
15
+ function actionsOf(entries: Readonly<Record<string, FlowAction>>): FlowActionRegistry {
16
+ const registry = createFlowActionRegistry();
17
+ for (const [name, action] of Object.entries(entries)) registry.register(name, action);
18
+ return registry;
19
+ }
20
+
21
+ interface RunExtra {
22
+ readonly signal?: AbortSignal | undefined;
23
+ readonly sleep?: ((ms: number, signal: AbortSignal) => Promise<void>) | undefined;
24
+ readonly now?: (() => number) | undefined;
25
+ }
26
+
27
+ async function runFlowWith(
28
+ flow: FlowDefinition,
29
+ actions: FlowActionRegistry,
30
+ input: unknown = {},
31
+ extra: RunExtra = {},
32
+ ): Promise<{ result: FlowResult; events: FlowStepEvent[] }> {
33
+ const events: FlowStepEvent[] = [];
34
+ const result = await runFlow({
35
+ flow,
36
+ input,
37
+ actions,
38
+ signal: extra.signal ?? new AbortController().signal,
39
+ runId: 'run-1',
40
+ workspace: '/w/a',
41
+ onStep: (event) => events.push(event),
42
+ ...(extra.sleep === undefined ? {} : { sleep: extra.sleep }),
43
+ ...(extra.now === undefined ? {} : { now: extra.now }),
44
+ });
45
+ return { result, events };
46
+ }
47
+
48
+ describe('runFlow', () => {
49
+ it('顺序跑三步,with 从前面步骤取值', async () => {
50
+ const seen: unknown[] = [];
51
+ const actions = actionsOf({
52
+ read: async () => ({ rows: [1, 2] }),
53
+ write: async (input) => {
54
+ seen.push(input);
55
+ return { count: 2 };
56
+ },
57
+ summary: async (input) => {
58
+ seen.push(input);
59
+ return 'ok';
60
+ },
61
+ });
62
+
63
+ const { result, events } = await runFlowWith(
64
+ {
65
+ id: 'check',
66
+ title: '检查',
67
+ target: ref('in.env'),
68
+ steps: [
69
+ { id: 'read', action: 'read' },
70
+ { id: 'write', action: 'write', with: { rows: ref('steps.read.rows') } },
71
+ { id: 'summary', action: 'summary', with: { count: ref('steps.write.count') } },
72
+ ],
73
+ },
74
+ actions,
75
+ { env: 'test' },
76
+ );
77
+
78
+ expect(result.status).toBe('ok');
79
+ expect(result.target).toBe('test');
80
+ expect(result.steps).toEqual({
81
+ read: { rows: [1, 2] },
82
+ write: { count: 2 },
83
+ summary: 'ok',
84
+ });
85
+ expect(seen).toEqual([{ rows: [1, 2] }, { count: 2 }]);
86
+ expect(events.filter((event) => event.status === 'ok').map((event) => event.stepId)).toEqual([
87
+ 'read',
88
+ 'write',
89
+ 'summary',
90
+ ]);
91
+ });
92
+
93
+ it('重试:前两次抛错,第三次成功', async () => {
94
+ let calls = 0;
95
+ const actions = actionsOf({
96
+ flaky: async () => {
97
+ calls += 1;
98
+ if (calls < 3) throw new Error('boom');
99
+ return 'ok';
100
+ },
101
+ });
102
+
103
+ const { result, events } = await runFlowWith(
104
+ {
105
+ id: 'retry',
106
+ title: '重试',
107
+ steps: [{ id: 'flaky', action: 'flaky', retry: { attempts: 2, delayMs: 10 } }],
108
+ },
109
+ actions,
110
+ {},
111
+ { sleep: async () => {} },
112
+ );
113
+
114
+ expect(result.status).toBe('ok');
115
+ expect(calls).toBe(3);
116
+ expect(
117
+ events.filter((event) => event.status === 'running').map((event) => event.attempt),
118
+ ).toEqual([1, 2, 3]);
119
+ });
120
+
121
+ it('轮询:直到 until 为真', async () => {
122
+ let calls = 0;
123
+ let clock = 0;
124
+ const actions = actionsOf({
125
+ job: async () => {
126
+ calls += 1;
127
+ return calls < 3 ? { status: 'pending' } : { status: 'done' };
128
+ },
129
+ });
130
+
131
+ const { result, events } = await runFlowWith(
132
+ {
133
+ id: 'poll',
134
+ title: '轮询',
135
+ steps: [
136
+ {
137
+ id: 'job',
138
+ action: 'job',
139
+ poll: {
140
+ everyMs: 100,
141
+ maxMs: 1000,
142
+ until: { $eq: [ref('value.status'), lit('done')] },
143
+ },
144
+ },
145
+ ],
146
+ },
147
+ actions,
148
+ {},
149
+ {
150
+ now: () => clock,
151
+ sleep: async (ms) => {
152
+ clock += ms;
153
+ },
154
+ },
155
+ );
156
+
157
+ expect(result.status).toBe('ok');
158
+ expect(calls).toBe(3);
159
+ expect(events.filter((event) => event.status === 'running')).toHaveLength(3);
160
+ });
161
+
162
+ it('轮询超时判失败,并指出步骤', async () => {
163
+ let clock = 0;
164
+ const actions = actionsOf({ job: async () => ({ status: 'pending' }) });
165
+
166
+ const { result } = await runFlowWith(
167
+ {
168
+ id: 'timeout',
169
+ title: '超时',
170
+ steps: [
171
+ {
172
+ id: 'job',
173
+ action: 'job',
174
+ poll: {
175
+ everyMs: 100,
176
+ maxMs: 250,
177
+ until: { $eq: [ref('value.status'), lit('done')] },
178
+ },
179
+ },
180
+ ],
181
+ },
182
+ actions,
183
+ {},
184
+ {
185
+ now: () => clock,
186
+ sleep: async (ms) => {
187
+ clock += ms;
188
+ },
189
+ },
190
+ );
191
+
192
+ expect(result.status).toBe('failed');
193
+ expect(result.failedStep).toBe('job');
194
+ expect(result.error).toBeInstanceOf(Error);
195
+ });
196
+
197
+ it('取消:结果是 stopped,后面的步骤不跑', async () => {
198
+ const controller = new AbortController();
199
+ const actions = actionsOf({
200
+ slow: async (_input, context) => {
201
+ await new Promise<void>((resolve) => {
202
+ if (context.signal.aborted) {
203
+ resolve();
204
+ return;
205
+ }
206
+ context.signal.addEventListener('abort', () => resolve(), { once: true });
207
+ });
208
+ throw new Error('aborted');
209
+ },
210
+ after: async () => 'no',
211
+ });
212
+
213
+ const running = runFlowWith(
214
+ {
215
+ id: 'cancel',
216
+ title: '取消',
217
+ steps: [
218
+ { id: 'slow', action: 'slow' },
219
+ { id: 'after', action: 'after' },
220
+ ],
221
+ },
222
+ actions,
223
+ {},
224
+ { signal: controller.signal },
225
+ );
226
+ await Promise.resolve();
227
+ controller.abort();
228
+ const { result, events } = await running;
229
+
230
+ expect(result.status).toBe('stopped');
231
+ expect(result.steps).toEqual({});
232
+ expect(events.some((event) => event.stepId === 'after')).toBe(false);
233
+ });
234
+
235
+ it('when 为假:跳过,不调动作', async () => {
236
+ let calls = 0;
237
+ const actions = actionsOf({
238
+ only: async () => {
239
+ calls += 1;
240
+ return 'ok';
241
+ },
242
+ });
243
+
244
+ const { result, events } = await runFlowWith(
245
+ {
246
+ id: 'skip',
247
+ title: '跳过',
248
+ steps: [{ id: 'only', action: 'only', when: { $eq: [ref('in.env'), lit('prod')] } }],
249
+ },
250
+ actions,
251
+ { env: 'test' },
252
+ );
253
+
254
+ expect(result.status).toBe('ok');
255
+ expect(result.steps).toEqual({});
256
+ expect(calls).toBe(0);
257
+ expect(events).toEqual([{ stepId: 'only', title: 'only', status: 'skipped' }]);
258
+ });
259
+
260
+ it('forEach:逐项串行,with 能取 item', async () => {
261
+ const seen: unknown[] = [];
262
+ const actions = actionsOf({
263
+ one: async (input) => {
264
+ seen.push(input);
265
+ return (input as { id: number }).id;
266
+ },
267
+ });
268
+
269
+ const { result } = await runFlowWith(
270
+ {
271
+ id: 'each',
272
+ title: '逐项',
273
+ steps: [
274
+ {
275
+ id: 'one',
276
+ action: 'one',
277
+ forEach: { over: ref('in.items'), as: 'item', max: 3 },
278
+ with: { id: ref('item.id') },
279
+ },
280
+ ],
281
+ },
282
+ actions,
283
+ { items: [{ id: 1 }, { id: 2 }] },
284
+ );
285
+
286
+ expect(result.status).toBe('ok');
287
+ expect(result.steps['one']).toEqual([1, 2]);
288
+ expect(seen).toEqual([{ id: 1 }, { id: 2 }]);
289
+ });
290
+
291
+ it('target 缺省回落流程 id', async () => {
292
+ const actions = actionsOf({ noop: async () => 'ok' });
293
+ const { result } = await runFlowWith(
294
+ { id: 'fallback', title: '回落', steps: [{ id: 'noop', action: 'noop' }] },
295
+ actions,
296
+ {},
297
+ );
298
+
299
+ expect(result.target).toBe('fallback');
300
+ });
301
+ });
@@ -0,0 +1,216 @@
1
+ import type {
2
+ FlowActionRegistry,
3
+ FlowCheck,
4
+ FlowDefinition,
5
+ FlowPredicate,
6
+ FlowProblem,
7
+ FlowValue,
8
+ } from './types.js';
9
+
10
+ type AddProblem = (path: string, problem: string) => void;
11
+
12
+ interface CheckContext {
13
+ readonly seen: ReadonlySet<string>;
14
+ readonly item: boolean;
15
+ readonly value: boolean;
16
+ }
17
+
18
+ function checkValue(value: FlowValue, path: string, context: CheckContext, add: AddProblem): void {
19
+ if ('$lit' in value) return;
20
+ if ('$default' in value) {
21
+ checkValue(value.$default[0], `${path}.$default[0]`, context, add);
22
+ checkValue(value.$default[1], `${path}.$default[1]`, context, add);
23
+ return;
24
+ }
25
+ if (!('$ref' in value)) {
26
+ add(path, '不是合法的取值表达式');
27
+ return;
28
+ }
29
+ const ref = value.$ref.trim();
30
+ if (ref === '') {
31
+ add(path, '$ref 不能为空');
32
+ return;
33
+ }
34
+ const parts = ref.split('.').filter((part) => part !== '');
35
+ const root = parts[0];
36
+ if (root === undefined) {
37
+ add(path, '$ref 不能为空');
38
+ return;
39
+ }
40
+ if (root === 'in') return;
41
+ if (root === 'steps') {
42
+ const id = parts[1];
43
+ if (id === undefined) {
44
+ add(path, '$ref 指向 steps 时要写到步骤 id');
45
+ return;
46
+ }
47
+ if (!context.seen.has(id)) add(path, `$ref 引用了还没出现的步骤:${id}`);
48
+ return;
49
+ }
50
+ if (root === 'item') {
51
+ if (!context.item) add(path, '$ref 的 item 只在 forEach 步骤里可用');
52
+ return;
53
+ }
54
+ if (root === 'value') {
55
+ if (!context.value) add(path, '$ref 的 value 只在 poll.until 里可用');
56
+ return;
57
+ }
58
+ add(path, `$ref 只能从 in / steps / item / value 起头:${ref}`);
59
+ }
60
+
61
+ function checkPredicate(
62
+ predicate: FlowPredicate,
63
+ path: string,
64
+ context: CheckContext,
65
+ add: AddProblem,
66
+ ): void {
67
+ if ('$eq' in predicate) {
68
+ checkValue(predicate.$eq[0], `${path}.$eq[0]`, context, add);
69
+ checkValue(predicate.$eq[1], `${path}.$eq[1]`, context, add);
70
+ return;
71
+ }
72
+ if ('$in' in predicate) {
73
+ checkValue(predicate.$in[0], `${path}.$in[0]`, context, add);
74
+ return;
75
+ }
76
+ if ('$exists' in predicate) {
77
+ checkValue(predicate.$exists, `${path}.$exists`, context, add);
78
+ return;
79
+ }
80
+ if ('$and' in predicate) {
81
+ predicate.$and.forEach((item, index) => {
82
+ checkPredicate(item, `${path}.$and[${index}]`, context, add);
83
+ });
84
+ return;
85
+ }
86
+ if ('$not' in predicate) {
87
+ checkPredicate(predicate.$not, `${path}.$not`, context, add);
88
+ return;
89
+ }
90
+ add(path, '不是合法的谓词');
91
+ }
92
+
93
+ function checkFlowAt(
94
+ flow: FlowDefinition,
95
+ actions: Pick<FlowActionRegistry, 'has'>,
96
+ prefix: string,
97
+ add: AddProblem,
98
+ ): void {
99
+ if (flow.id.trim() === '') add(`${prefix}id`, '流程 id 不能为空');
100
+ if (flow.title.trim() === '') add(`${prefix}title`, '流程标题不能为空');
101
+ if (flow.steps.length === 0) add(`${prefix}steps`, '至少要有一步');
102
+ if (flow.target !== undefined) {
103
+ checkValue(flow.target, `${prefix}target`, { seen: new Set(), item: false, value: false }, add);
104
+ }
105
+ if (flow.stopWhen !== undefined) {
106
+ const all = new Set(flow.steps.map((step) => step.id));
107
+ checkPredicate(flow.stopWhen, `${prefix}stopWhen`, { seen: all, item: false, value: false }, add);
108
+ }
109
+
110
+ const seen = new Set<string>();
111
+ flow.steps.forEach((step, index) => {
112
+ const at = `${prefix}steps[${index}]`;
113
+ if (step.id.trim() === '') add(`${at}.id`, '步骤 id 不能为空');
114
+ if (seen.has(step.id)) add(`${at}.id`, `步骤 id 重复:${step.id}`);
115
+
116
+ if ('flow' in step) {
117
+ if (step.when !== undefined) {
118
+ checkPredicate(step.when, `${at}.when`, { seen, item: false, value: false }, add);
119
+ }
120
+ if (step.with !== undefined) {
121
+ for (const [key, value] of Object.entries(step.with)) {
122
+ checkValue(value, `${at}.with.${key}`, { seen, item: false, value: false }, add);
123
+ }
124
+ }
125
+ checkFlowAt(step.flow, actions, `${at}.flow.`, add);
126
+ seen.add(step.id);
127
+ return;
128
+ }
129
+
130
+ if (step.action.trim() === '') add(`${at}.action`, '动作名不能为空');
131
+ else if (!actions.has(step.action)) add(`${at}.action`, `注册表里没有这个动作:${step.action}`);
132
+
133
+ const item = step.forEach !== undefined;
134
+ if (step.when !== undefined) {
135
+ checkPredicate(step.when, `${at}.when`, { seen, item: false, value: false }, add);
136
+ }
137
+ if (step.with !== undefined) {
138
+ for (const [key, value] of Object.entries(step.with)) {
139
+ checkValue(value, `${at}.with.${key}`, { seen, item, value: false }, add);
140
+ }
141
+ }
142
+
143
+ if (step.forEach !== undefined) {
144
+ checkValue(step.forEach.over, `${at}.forEach.over`, { seen, item: false, value: false }, add);
145
+ if (step.forEach.as.trim() === '') add(`${at}.forEach.as`, 'forEach.as 不能为空');
146
+ if (!Number.isInteger(step.forEach.max) || step.forEach.max < 1 || step.forEach.max > 50) {
147
+ add(`${at}.forEach.max`, 'forEach.max 要在 1..50');
148
+ }
149
+ }
150
+
151
+ if (step.retry !== undefined) {
152
+ if (
153
+ !Number.isInteger(step.retry.attempts) ||
154
+ step.retry.attempts < 0 ||
155
+ step.retry.attempts > 5
156
+ ) {
157
+ add(`${at}.retry.attempts`, 'retry.attempts 要在 0..5');
158
+ }
159
+ if (
160
+ step.retry.delayMs !== undefined &&
161
+ (!Number.isInteger(step.retry.delayMs) ||
162
+ step.retry.delayMs < 0 ||
163
+ step.retry.delayMs > 60_000)
164
+ ) {
165
+ add(`${at}.retry.delayMs`, 'retry.delayMs 要在 0..60000');
166
+ }
167
+ }
168
+
169
+ if (step.poll !== undefined) {
170
+ if (
171
+ !Number.isInteger(step.poll.everyMs) ||
172
+ step.poll.everyMs < 100 ||
173
+ step.poll.everyMs > 60_000
174
+ ) {
175
+ add(`${at}.poll.everyMs`, 'poll.everyMs 要在 100..60000');
176
+ }
177
+ if (
178
+ !Number.isInteger(step.poll.maxMs) ||
179
+ step.poll.maxMs < 0 ||
180
+ step.poll.maxMs > 1_800_000
181
+ ) {
182
+ add(`${at}.poll.maxMs`, 'poll.maxMs 要在 0..1800000');
183
+ } else if (step.poll.maxMs !== 0 && step.poll.maxMs < step.poll.everyMs) {
184
+ add(`${at}.poll.maxMs`, 'poll.maxMs 不能小于 everyMs');
185
+ }
186
+ if (
187
+ step.poll.onTimeout !== undefined &&
188
+ step.poll.onTimeout !== 'fail' &&
189
+ step.poll.onTimeout !== 'continue'
190
+ ) {
191
+ add(`${at}.poll.onTimeout`, 'poll.onTimeout 只能是 fail 或 continue');
192
+ }
193
+ checkPredicate(step.poll.until, `${at}.poll.until`, { seen, item, value: true }, add);
194
+ }
195
+
196
+ seen.add(step.id);
197
+ });
198
+ }
199
+
200
+ /**
201
+ * 校验一条流程:id / 步骤、动作存在、引用只指前面、重试与轮询在边界内、子流程也照查。
202
+ * 只做静态检查,不碰平台、不跑动作。
203
+ * @param flow 流程定义。
204
+ * @param actions 动作表(只问 `has`)。
205
+ * @returns 逐条问题;`ok` 为真表示没有。
206
+ */
207
+ export function checkFlow(
208
+ flow: FlowDefinition,
209
+ actions: Pick<FlowActionRegistry, 'has'>,
210
+ ): FlowCheck {
211
+ const problems: FlowProblem[] = [];
212
+ checkFlowAt(flow, actions, '', (path, problem) => {
213
+ problems.push({ path, problem });
214
+ });
215
+ return { ok: problems.length === 0, problems };
216
+ }
@@ -0,0 +1,94 @@
1
+ import { isDeepStrictEqual } from 'node:util';
2
+ import type { FlowPredicate, FlowScope, FlowValue } from './types.js';
3
+
4
+ function fieldOf(value: unknown, key: string): unknown {
5
+ if (Array.isArray(value)) {
6
+ const index = Number.parseInt(key, 10);
7
+ return Number.isInteger(index) && index >= 0 ? value[index] : undefined;
8
+ }
9
+ if (typeof value !== 'object' || value === null) return undefined;
10
+ return (value as Readonly<Record<string, unknown>>)[key];
11
+ }
12
+
13
+ /**
14
+ * 读一个引用路径:`in.env`、`steps.builds.rows.0.id`、`item.id`、`value.status`。
15
+ * 中间缺一段就给 `undefined`,由调用方决定这是不是错。
16
+ * @param path 点分路径。
17
+ * @param scope 求值现场。
18
+ * @returns 取到的值;路径不成立给 `undefined`。
19
+ */
20
+ export function readRef(path: string, scope: FlowScope): unknown {
21
+ const parts = path.split('.').filter((part) => part !== '');
22
+ const [root, ...rest] = parts;
23
+ if (root === undefined) return undefined;
24
+ let current: unknown;
25
+ if (root === 'in') current = scope.in;
26
+ else if (root === 'item') current = scope.item;
27
+ else if (root === 'value') current = scope.value;
28
+ else if (root === 'steps') {
29
+ const id = rest.shift();
30
+ if (id === undefined) return undefined;
31
+ current = scope.steps[id];
32
+ } else return undefined;
33
+ for (const part of rest) current = fieldOf(current, part);
34
+ return current;
35
+ }
36
+
37
+ /**
38
+ * 求一个取值表达式:字面量、引用,或二选一。
39
+ * @param value 表达式。
40
+ * @param scope 求值现场。
41
+ * @returns 求出来的值;引用不成立给 `undefined`。
42
+ */
43
+ export function resolveValue(value: FlowValue, scope: FlowScope): unknown {
44
+ if ('$lit' in value) return value.$lit;
45
+ if ('$ref' in value) return readRef(value.$ref, scope);
46
+ if ('$default' in value) {
47
+ const first = resolveValue(value.$default[0], scope);
48
+ return first === undefined ? resolveValue(value.$default[1], scope) : first;
49
+ }
50
+ return undefined;
51
+ }
52
+
53
+ function sameValue(left: unknown, right: unknown): boolean {
54
+ return isDeepStrictEqual(left, right);
55
+ }
56
+
57
+ /**
58
+ * 判一个条件表达式。只认五个操作符;未知形状给 `false`。
59
+ * @param predicate 条件表达式。
60
+ * @param scope 求值现场。
61
+ * @returns 真假。
62
+ */
63
+ export function evalPredicate(predicate: FlowPredicate, scope: FlowScope): boolean {
64
+ if ('$eq' in predicate) {
65
+ return sameValue(resolveValue(predicate.$eq[0], scope), resolveValue(predicate.$eq[1], scope));
66
+ }
67
+ if ('$in' in predicate) {
68
+ const needle = resolveValue(predicate.$in[0], scope);
69
+ return predicate.$in[1].some((candidate) => sameValue(candidate, needle));
70
+ }
71
+ if ('$exists' in predicate) {
72
+ const value = resolveValue(predicate.$exists, scope);
73
+ return value !== undefined && value !== null;
74
+ }
75
+ if ('$and' in predicate) return predicate.$and.every((item) => evalPredicate(item, scope));
76
+ if ('$not' in predicate) return !evalPredicate(predicate.$not, scope);
77
+ return false;
78
+ }
79
+
80
+ /**
81
+ * 把步骤的 `with` 映射求成动作入参。
82
+ * @param values 字段到表达式的映射;没给就是空对象。
83
+ * @param scope 求值现场。
84
+ * @returns 动作入参。
85
+ */
86
+ export function resolveInput(
87
+ values: Readonly<Record<string, FlowValue>> | undefined,
88
+ scope: FlowScope,
89
+ ): Readonly<Record<string, unknown>> {
90
+ if (values === undefined) return {};
91
+ const input: Record<string, unknown> = {};
92
+ for (const [key, value] of Object.entries(values)) input[key] = resolveValue(value, scope);
93
+ return input;
94
+ }
@@ -0,0 +1,30 @@
1
+ import { checkFlow } from './check.js';
2
+ import { createFlowActionRegistry, runFlow } from './run.js';
3
+ import type {
4
+ FlowActionRegistry,
5
+ FlowCheck,
6
+ FlowDefinition,
7
+ FlowResult,
8
+ FlowRunOptions,
9
+ } from './types.js';
10
+
11
+ /**
12
+ * 流程引擎的宿主 kit:模块从 `mount(deps).flow` 拿,不直接 import shared。
13
+ */
14
+ export interface FlowHost {
15
+ createActions(): FlowActionRegistry;
16
+ check(flow: FlowDefinition, actions: Pick<FlowActionRegistry, 'has'>): FlowCheck;
17
+ run(options: FlowRunOptions): Promise<FlowResult>;
18
+ }
19
+
20
+ /**
21
+ * 建一份无状态的流程宿主 kit。
22
+ * @returns `createActions` / `check` / `run`。
23
+ */
24
+ export function createFlowHost(): FlowHost {
25
+ return {
26
+ createActions: () => createFlowActionRegistry(),
27
+ check: (flow, actions) => checkFlow(flow, actions),
28
+ run: (options) => runFlow(options),
29
+ };
30
+ }