@relayflows/sdk 2.0.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 (231) hide show
  1. package/dist/authored-flow-error.d.ts +9 -0
  2. package/dist/authored-flow-error.d.ts.map +1 -0
  3. package/dist/authored-flow-error.js +13 -0
  4. package/dist/authored-flow-error.js.map +1 -0
  5. package/dist/authored-flow-executor.d.ts +28 -0
  6. package/dist/authored-flow-executor.d.ts.map +1 -0
  7. package/dist/authored-flow-executor.js +203 -0
  8. package/dist/authored-flow-executor.js.map +1 -0
  9. package/dist/authored-flow-lifecycle.d.ts +61 -0
  10. package/dist/authored-flow-lifecycle.d.ts.map +1 -0
  11. package/dist/authored-flow-lifecycle.js +284 -0
  12. package/dist/authored-flow-lifecycle.js.map +1 -0
  13. package/dist/authored-flow-loader.d.ts +7 -0
  14. package/dist/authored-flow-loader.d.ts.map +1 -0
  15. package/dist/authored-flow-loader.js +39 -0
  16. package/dist/authored-flow-loader.js.map +1 -0
  17. package/dist/authored-flow-operation.d.ts +43 -0
  18. package/dist/authored-flow-operation.d.ts.map +1 -0
  19. package/dist/authored-flow-operation.js +176 -0
  20. package/dist/authored-flow-operation.js.map +1 -0
  21. package/dist/authored-flow.d.ts +12 -0
  22. package/dist/authored-flow.d.ts.map +1 -0
  23. package/dist/authored-flow.js +13 -0
  24. package/dist/authored-flow.js.map +1 -0
  25. package/dist/authored-promise-graph.d.ts +67 -0
  26. package/dist/authored-promise-graph.d.ts.map +1 -0
  27. package/dist/authored-promise-graph.js +206 -0
  28. package/dist/authored-promise-graph.js.map +1 -0
  29. package/dist/backlog-picker.d.ts +55 -0
  30. package/dist/backlog-picker.d.ts.map +1 -0
  31. package/dist/backlog-picker.js +117 -0
  32. package/dist/backlog-picker.js.map +1 -0
  33. package/dist/canonical.d.ts +18 -0
  34. package/dist/canonical.d.ts.map +1 -0
  35. package/dist/canonical.js +55 -0
  36. package/dist/canonical.js.map +1 -0
  37. package/dist/cli/check.d.ts +30 -0
  38. package/dist/cli/check.d.ts.map +1 -0
  39. package/dist/cli/check.js +324 -0
  40. package/dist/cli/check.js.map +1 -0
  41. package/dist/cli/direct-run.d.ts +3 -0
  42. package/dist/cli/direct-run.d.ts.map +1 -0
  43. package/dist/cli/direct-run.js +69 -0
  44. package/dist/cli/direct-run.js.map +1 -0
  45. package/dist/cli/hn-monitor.d.ts +107 -0
  46. package/dist/cli/hn-monitor.d.ts.map +1 -0
  47. package/dist/cli/hn-monitor.js +220 -0
  48. package/dist/cli/hn-monitor.js.map +1 -0
  49. package/dist/cli/interruptible-sleep.d.ts +13 -0
  50. package/dist/cli/interruptible-sleep.d.ts.map +1 -0
  51. package/dist/cli/interruptible-sleep.js +31 -0
  52. package/dist/cli/interruptible-sleep.js.map +1 -0
  53. package/dist/cli/run.d.ts +54 -0
  54. package/dist/cli/run.d.ts.map +1 -0
  55. package/dist/cli/run.js +316 -0
  56. package/dist/cli/run.js.map +1 -0
  57. package/dist/cli/tick-runner.d.ts +151 -0
  58. package/dist/cli/tick-runner.d.ts.map +1 -0
  59. package/dist/cli/tick-runner.js +258 -0
  60. package/dist/cli/tick-runner.js.map +1 -0
  61. package/dist/cli-adapter.d.ts +29 -0
  62. package/dist/cli-adapter.d.ts.map +1 -0
  63. package/dist/cli-adapter.js +92 -0
  64. package/dist/cli-adapter.js.map +1 -0
  65. package/dist/cli-executable.d.ts +3 -0
  66. package/dist/cli-executable.d.ts.map +1 -0
  67. package/dist/cli-executable.js +10 -0
  68. package/dist/cli-executable.js.map +1 -0
  69. package/dist/cli.d.ts +9 -0
  70. package/dist/cli.d.ts.map +1 -0
  71. package/dist/cli.js +348 -0
  72. package/dist/cli.js.map +1 -0
  73. package/dist/compile.d.ts +42 -0
  74. package/dist/compile.d.ts.map +1 -0
  75. package/dist/compile.js +515 -0
  76. package/dist/compile.js.map +1 -0
  77. package/dist/demo-hn-monitor.d.ts +2 -0
  78. package/dist/demo-hn-monitor.d.ts.map +1 -0
  79. package/dist/demo-hn-monitor.js +118 -0
  80. package/dist/demo-hn-monitor.js.map +1 -0
  81. package/dist/dir-watcher-poller.d.ts +73 -0
  82. package/dist/dir-watcher-poller.d.ts.map +1 -0
  83. package/dist/dir-watcher-poller.js +80 -0
  84. package/dist/dir-watcher-poller.js.map +1 -0
  85. package/dist/direct-input.d.ts +10 -0
  86. package/dist/direct-input.d.ts.map +1 -0
  87. package/dist/direct-input.js +61 -0
  88. package/dist/direct-input.js.map +1 -0
  89. package/dist/failure-kinds.d.ts +25 -0
  90. package/dist/failure-kinds.d.ts.map +1 -0
  91. package/dist/failure-kinds.js +64 -0
  92. package/dist/failure-kinds.js.map +1 -0
  93. package/dist/gate-contract.d.ts +24 -0
  94. package/dist/gate-contract.d.ts.map +1 -0
  95. package/dist/gate-contract.js +50 -0
  96. package/dist/gate-contract.js.map +1 -0
  97. package/dist/hn-poller.d.ts +50 -0
  98. package/dist/hn-poller.d.ts.map +1 -0
  99. package/dist/hn-poller.js +79 -0
  100. package/dist/hn-poller.js.map +1 -0
  101. package/dist/index.d.ts +21 -0
  102. package/dist/index.d.ts.map +1 -0
  103. package/dist/index.js +30 -0
  104. package/dist/index.js.map +1 -0
  105. package/dist/journal-client.d.ts +122 -0
  106. package/dist/journal-client.d.ts.map +1 -0
  107. package/dist/journal-client.js +277 -0
  108. package/dist/journal-client.js.map +1 -0
  109. package/dist/json-schema-bound.d.ts +7 -0
  110. package/dist/json-schema-bound.d.ts.map +1 -0
  111. package/dist/json-schema-bound.js +353 -0
  112. package/dist/json-schema-bound.js.map +1 -0
  113. package/dist/json-schema.d.ts +5 -0
  114. package/dist/json-schema.d.ts.map +1 -0
  115. package/dist/json-schema.js +72 -0
  116. package/dist/json-schema.js.map +1 -0
  117. package/dist/json-value.d.ts +6 -0
  118. package/dist/json-value.d.ts.map +1 -0
  119. package/dist/json-value.js +94 -0
  120. package/dist/json-value.js.map +1 -0
  121. package/dist/model-name.d.ts +7 -0
  122. package/dist/model-name.d.ts.map +1 -0
  123. package/dist/model-name.js +21 -0
  124. package/dist/model-name.js.map +1 -0
  125. package/dist/output-schema.d.ts +8 -0
  126. package/dist/output-schema.d.ts.map +1 -0
  127. package/dist/output-schema.js +28 -0
  128. package/dist/output-schema.js.map +1 -0
  129. package/dist/preflight.d.ts +80 -0
  130. package/dist/preflight.d.ts.map +1 -0
  131. package/dist/preflight.js +361 -0
  132. package/dist/preflight.js.map +1 -0
  133. package/dist/protocol.d.ts +359 -0
  134. package/dist/protocol.d.ts.map +1 -0
  135. package/dist/protocol.js +15 -0
  136. package/dist/protocol.js.map +1 -0
  137. package/dist/spec.d.ts +294 -0
  138. package/dist/spec.d.ts.map +1 -0
  139. package/dist/spec.js +13 -0
  140. package/dist/spec.js.map +1 -0
  141. package/dist/step-dependencies.d.ts +2 -0
  142. package/dist/step-dependencies.d.ts.map +1 -0
  143. package/dist/step-dependencies.js +88 -0
  144. package/dist/step-dependencies.js.map +1 -0
  145. package/dist/step-fields.d.ts +17 -0
  146. package/dist/step-fields.d.ts.map +1 -0
  147. package/dist/step-fields.js +32 -0
  148. package/dist/step-fields.js.map +1 -0
  149. package/dist/tick-source.d.ts +205 -0
  150. package/dist/tick-source.d.ts.map +1 -0
  151. package/dist/tick-source.js +234 -0
  152. package/dist/tick-source.js.map +1 -0
  153. package/dist/unknown-keys.d.ts +7 -0
  154. package/dist/unknown-keys.d.ts.map +1 -0
  155. package/dist/unknown-keys.js +47 -0
  156. package/dist/unknown-keys.js.map +1 -0
  157. package/dist/validate.d.ts +7 -0
  158. package/dist/validate.d.ts.map +1 -0
  159. package/dist/validate.js +458 -0
  160. package/dist/validate.js.map +1 -0
  161. package/dist/work-package-consumer.d.ts +33 -0
  162. package/dist/work-package-consumer.d.ts.map +1 -0
  163. package/dist/work-package-consumer.js +44 -0
  164. package/dist/work-package-consumer.js.map +1 -0
  165. package/dist/work-package-validator.d.ts +15 -0
  166. package/dist/work-package-validator.d.ts.map +1 -0
  167. package/dist/work-package-validator.js +72 -0
  168. package/dist/work-package-validator.js.map +1 -0
  169. package/dist/worker-cli.d.ts +16 -0
  170. package/dist/worker-cli.d.ts.map +1 -0
  171. package/dist/worker-cli.js +77 -0
  172. package/dist/worker-cli.js.map +1 -0
  173. package/dist/worker.d.ts +57 -0
  174. package/dist/worker.d.ts.map +1 -0
  175. package/dist/worker.js +129 -0
  176. package/dist/worker.js.map +1 -0
  177. package/dist/wrapper-runtime.d.ts +10 -0
  178. package/dist/wrapper-runtime.d.ts.map +1 -0
  179. package/dist/wrapper-runtime.js +83 -0
  180. package/dist/wrapper-runtime.js.map +1 -0
  181. package/dist/wrapper-session.d.ts +16 -0
  182. package/dist/wrapper-session.d.ts.map +1 -0
  183. package/dist/wrapper-session.js +244 -0
  184. package/dist/wrapper-session.js.map +1 -0
  185. package/package.json +50 -0
  186. package/src/authored-flow-error.ts +31 -0
  187. package/src/authored-flow-executor.ts +365 -0
  188. package/src/authored-flow-lifecycle.ts +344 -0
  189. package/src/authored-flow-loader.ts +44 -0
  190. package/src/authored-flow-operation.ts +252 -0
  191. package/src/authored-flow.ts +21 -0
  192. package/src/authored-promise-graph.ts +196 -0
  193. package/src/backlog-picker.ts +153 -0
  194. package/src/canonical.ts +57 -0
  195. package/src/cli/check.ts +391 -0
  196. package/src/cli/direct-run.ts +86 -0
  197. package/src/cli/hn-monitor.ts +286 -0
  198. package/src/cli/interruptible-sleep.ts +27 -0
  199. package/src/cli/run.ts +439 -0
  200. package/src/cli/tick-runner.ts +378 -0
  201. package/src/cli-adapter.ts +115 -0
  202. package/src/cli-executable.ts +12 -0
  203. package/src/cli.ts +364 -0
  204. package/src/compile.ts +573 -0
  205. package/src/demo-hn-monitor.ts +132 -0
  206. package/src/dir-watcher-poller.ts +122 -0
  207. package/src/direct-input.ts +69 -0
  208. package/src/failure-kinds.ts +77 -0
  209. package/src/gate-contract.ts +72 -0
  210. package/src/hn-poller.ts +104 -0
  211. package/src/index.ts +195 -0
  212. package/src/journal-client.ts +379 -0
  213. package/src/json-schema-bound.ts +357 -0
  214. package/src/json-schema.ts +65 -0
  215. package/src/json-value.ts +110 -0
  216. package/src/model-name.ts +20 -0
  217. package/src/output-schema.ts +33 -0
  218. package/src/preflight.ts +482 -0
  219. package/src/protocol.ts +364 -0
  220. package/src/spec.ts +343 -0
  221. package/src/step-dependencies.ts +100 -0
  222. package/src/step-fields.ts +36 -0
  223. package/src/tick-source.ts +334 -0
  224. package/src/unknown-keys.ts +52 -0
  225. package/src/validate.ts +503 -0
  226. package/src/work-package-consumer.ts +73 -0
  227. package/src/work-package-validator.ts +90 -0
  228. package/src/worker-cli.ts +110 -0
  229. package/src/worker.ts +150 -0
  230. package/src/wrapper-runtime.ts +93 -0
  231. package/src/wrapper-session.ts +287 -0
@@ -0,0 +1,44 @@
1
+ import { accessSync, constants } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ import { getAuthoredFlowDefinition, type FlowHandle } from './authored-flow.js';
5
+
6
+ export class AuthoredFlowLoadError extends Error {
7
+ constructor(message: string) {
8
+ super(message);
9
+ this.name = 'AuthoredFlowLoadError';
10
+ }
11
+ }
12
+
13
+ /** Import and validate a direct-run module without executing its authored body. */
14
+ export async function loadAuthoredFlow(path: string): Promise<FlowHandle> {
15
+ const absolutePath = resolve(path);
16
+ try {
17
+ accessSync(absolutePath, constants.R_OK);
18
+ } catch {
19
+ throw new AuthoredFlowLoadError(`Flow "${path}" is not readable.`);
20
+ }
21
+
22
+ let authoredModule: Record<string, unknown>;
23
+ try {
24
+ authoredModule = await import(pathToFileURL(absolutePath).href) as Record<string, unknown>;
25
+ } catch (error) {
26
+ throw new AuthoredFlowLoadError(
27
+ `Flow "${path}" could not be imported: ${errorMessage(error)}`,
28
+ );
29
+ }
30
+
31
+ const handle = authoredModule['default'] as FlowHandle;
32
+ try {
33
+ getAuthoredFlowDefinition(handle);
34
+ } catch (error) {
35
+ throw new AuthoredFlowLoadError(
36
+ `Flow "${path}" must default-export flow(...): ${errorMessage(error)}`,
37
+ );
38
+ }
39
+ return handle;
40
+ }
41
+
42
+ function errorMessage(error: unknown): string {
43
+ return error instanceof Error ? error.message : 'unknown authored-flow error';
44
+ }
@@ -0,0 +1,252 @@
1
+ import { executionAsyncId } from 'node:async_hooks';
2
+ import type { Step } from '@relayflows/surface';
3
+ import { AuthoredFlowExecutionError } from './authored-flow-error.js';
4
+ import {
5
+ AuthoredFlowLifecycle,
6
+ type AuthoredOperationInvocation,
7
+ } from './authored-flow-lifecycle.js';
8
+
9
+ type OperationState = 'created' | 'running' | 'fulfilled' | 'rejected';
10
+ const nativePromiseThen = Promise.prototype.then;
11
+
12
+ /** A root authored operation whose outcome cannot be hidden by promise handlers. */
13
+ export class AuthoredFlowOperation<T> {
14
+ readonly step: Step<T>;
15
+ private state: OperationState = 'created';
16
+ private thenInvoked = false;
17
+ private rootFailureRecorded = false;
18
+ private rootFailure: unknown;
19
+ private callbackFailureRecorded = false;
20
+ private callbackFailure: unknown;
21
+ private readonly promise: Promise<T>;
22
+ private readonly resolve: (value: T | PromiseLike<T>) => void;
23
+ private readonly reject: (reason?: unknown) => void;
24
+
25
+ constructor(
26
+ readonly id: string,
27
+ readonly verb: string,
28
+ private readonly assertCanStart: () => void,
29
+ private readonly start: () => Promise<T>,
30
+ private readonly scope: AuthoredFlowLifecycle,
31
+ ) {
32
+ let resolve!: (value: T | PromiseLike<T>) => void;
33
+ let reject!: (reason?: unknown) => void;
34
+ this.promise = new Promise<T>((promiseResolve, promiseReject) => {
35
+ resolve = promiseResolve;
36
+ reject = promiseReject;
37
+ });
38
+ this.resolve = resolve;
39
+ this.reject = reject;
40
+
41
+ observeRejection(this.promise, (error) => this.recordRootFailure(error));
42
+ const operation = this;
43
+ this.step = Object.freeze({
44
+ gate(): never {
45
+ throw new AuthoredFlowExecutionError(
46
+ 'unsupported_gate',
47
+ 'postfix gates are not lowered by the initial authored executor',
48
+ );
49
+ },
50
+ then<TResult1 = T, TResult2 = never>(
51
+ onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
52
+ onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
53
+ ): Promise<TResult1 | TResult2> {
54
+ operation.thenInvoked = true;
55
+ const invocation = operation.scope.registerInvocation(operation, executionAsyncId());
56
+ void operation.begin();
57
+ const derived = nativeThen(
58
+ operation.promise,
59
+ wrapResolver(operation, invocation, onfulfilled),
60
+ wrapResolver(operation, invocation, onrejected),
61
+ );
62
+ return trackDerivedPromise(
63
+ derived,
64
+ (error) => operation.recordCallbackFailure(error),
65
+ );
66
+ },
67
+ });
68
+ this.scope.registerStep(this.step, this);
69
+ }
70
+
71
+ get lifecycle(): OperationState {
72
+ return this.state;
73
+ }
74
+
75
+ get wasManuallyChained(): boolean {
76
+ return this.thenInvoked && !this.scope.hasBoundConsumer(this);
77
+ }
78
+
79
+ get wasAwaited(): boolean {
80
+ return this.scope.hasBoundConsumer(this);
81
+ }
82
+
83
+ get failure(): { readonly recorded: boolean; readonly value: unknown } {
84
+ return { recorded: this.rootFailureRecorded, value: this.rootFailure };
85
+ }
86
+
87
+ get derivedFailure(): { readonly recorded: boolean; readonly value: unknown } {
88
+ return { recorded: this.callbackFailureRecorded, value: this.callbackFailure };
89
+ }
90
+
91
+ cancel(error: unknown): void {
92
+ if (this.state !== 'created') return;
93
+ this.state = 'rejected';
94
+ this.recordRootFailure(error);
95
+ this.reject(error);
96
+ }
97
+
98
+ async waitForSettlement(): Promise<void> {
99
+ await nativeThen(this.promise, () => undefined, () => undefined);
100
+ }
101
+
102
+ invokeResolver<TResult>(
103
+ invocation: AuthoredOperationInvocation,
104
+ callback: () => TResult,
105
+ ): TResult {
106
+ return this.scope.invokeResolver(invocation, callback);
107
+ }
108
+
109
+ private async begin(): Promise<void> {
110
+ if (this.state !== 'created') return;
111
+ try {
112
+ this.assertCanStart();
113
+ this.state = 'running';
114
+ const value = await this.start();
115
+ this.state = 'fulfilled';
116
+ this.resolve(value);
117
+ } catch (error) {
118
+ this.state = 'rejected';
119
+ this.recordRootFailure(error);
120
+ this.reject(error);
121
+ }
122
+ }
123
+
124
+ private recordRootFailure(error: unknown): void {
125
+ if (this.rootFailureRecorded) return;
126
+ this.rootFailureRecorded = true;
127
+ this.rootFailure = error;
128
+ }
129
+
130
+ private recordCallbackFailure(error: unknown): void {
131
+ if (this.callbackFailureRecorded) return;
132
+ this.callbackFailureRecorded = true;
133
+ this.callbackFailure = error;
134
+ }
135
+ }
136
+
137
+ export async function verifyAuthoredOperations(
138
+ flowName: string,
139
+ operations: readonly AuthoredFlowOperation<unknown>[],
140
+ lifecycle: AuthoredFlowLifecycle,
141
+ ): Promise<void> {
142
+ const canceled = new Set(operations.filter((operation) => operation.lifecycle === 'created'));
143
+
144
+ if (canceled.size > 0) {
145
+ const error = unawaitedError(flowName, [...canceled]);
146
+ for (const operation of operations) operation.cancel(error);
147
+ }
148
+
149
+ // Read the in-flight set before the gate awaits anything: "was derived work
150
+ // still running when the body returned" is the question, and every await here
151
+ // would move the answer.
152
+ const inFlight = lifecycle.derivedWorkInFlight(operations);
153
+
154
+ await Promise.all(operations.map((operation) => operation.waitForSettlement()));
155
+ await lifecycle.observeCallbackFailures(operations);
156
+ const unawaited = operations.filter((operation) =>
157
+ !lifecycle.isHandled(operation)
158
+ || operation.lifecycle === 'created'
159
+ || operation.lifecycle === 'running');
160
+
161
+ const failed = operations.find((operation) =>
162
+ operation.failure.recorded && !canceled.has(operation));
163
+ if (failed !== undefined) {
164
+ throw failed.failure.value;
165
+ }
166
+ const callbackFailed = operations.find((operation) =>
167
+ operation.derivedFailure.recorded || lifecycle.callbackFailure(operation).recorded);
168
+ if (callbackFailed !== undefined) {
169
+ const failure = callbackFailed.derivedFailure.recorded
170
+ ? callbackFailed.derivedFailure.value
171
+ : lifecycle.callbackFailure(callbackFailed).value;
172
+ throw new AuthoredFlowExecutionError(
173
+ 'operation_callback_failed',
174
+ `flow "${flowName}" derived handler for ${formatOperation(callbackFailed)} rejected: ${describeError(failure)}`,
175
+ );
176
+ }
177
+ if (unawaited.length > 0) {
178
+ throw unawaitedError(flowName, unawaited);
179
+ }
180
+ if (inFlight.length > 0) {
181
+ throw new AuthoredFlowExecutionError(
182
+ 'unsettled_derived_work',
183
+ `flow "${flowName}" completed while work derived from ${inFlight.map(formatOperation).join(', ')} was still in flight`,
184
+ );
185
+ }
186
+ }
187
+
188
+ export async function stopAuthoredOperations(
189
+ operations: readonly AuthoredFlowOperation<unknown>[],
190
+ reason: unknown,
191
+ ): Promise<void> {
192
+ for (const operation of operations) operation.cancel(reason);
193
+ await Promise.all(operations.map((operation) => operation.waitForSettlement()));
194
+ }
195
+
196
+ function unawaitedError(
197
+ flowName: string,
198
+ operations: readonly AuthoredFlowOperation<unknown>[],
199
+ ): AuthoredFlowExecutionError {
200
+ return new AuthoredFlowExecutionError(
201
+ 'unawaited_step',
202
+ `flow "${flowName}" returned with unawaited steps: ${operations.map(formatOperation).join(', ')}`,
203
+ );
204
+ }
205
+
206
+ function formatOperation(operation: AuthoredFlowOperation<unknown>): string {
207
+ return `${operation.id} (f.${operation.verb})`;
208
+ }
209
+
210
+ function describeError(error: unknown): string {
211
+ return error instanceof Error ? error.message : String(error);
212
+ }
213
+
214
+ function observeRejection<T>(promise: Promise<T>, record: (error: unknown) => void): void {
215
+ void nativeThen(promise, undefined, (error) => record(error));
216
+ }
217
+
218
+ function nativeThen<T, TResult1 = T, TResult2 = never>(
219
+ promise: Promise<T>,
220
+ onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
221
+ onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
222
+ ): Promise<TResult1 | TResult2> {
223
+ return nativePromiseThen.call(promise, onfulfilled, onrejected) as Promise<TResult1 | TResult2>;
224
+ }
225
+
226
+ function wrapResolver<T, TValue, TResult>(
227
+ operation: AuthoredFlowOperation<T>,
228
+ invocation: AuthoredOperationInvocation,
229
+ callback?: ((value: TValue) => TResult | PromiseLike<TResult>) | null,
230
+ ): ((value: TValue) => TResult | PromiseLike<TResult>) | undefined {
231
+ if (callback === undefined || callback === null) return undefined;
232
+ return (value) => operation.invokeResolver(invocation, () => callback(value));
233
+ }
234
+
235
+ function trackDerivedPromise<T>(
236
+ promise: Promise<T>,
237
+ record: (error: unknown) => void,
238
+ ): Promise<T> {
239
+ observeRejection(promise, record);
240
+ Object.defineProperty(promise, 'then', {
241
+ value<TResult1 = T, TResult2 = never>(
242
+ onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
243
+ onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
244
+ ): Promise<TResult1 | TResult2> {
245
+ return trackDerivedPromise(
246
+ nativeThen(promise, onfulfilled, onrejected),
247
+ record,
248
+ );
249
+ },
250
+ });
251
+ return promise;
252
+ }
@@ -0,0 +1,21 @@
1
+ import {
2
+ getFlowDefinition,
3
+ type AuthoredFlowDefinition,
4
+ type FlowHandle,
5
+ } from '@relayflows/surface/runtime';
6
+
7
+ /**
8
+ * Recover the immutable program retained by an authored flow handle.
9
+ *
10
+ * Importing the module evaluates author code only far enough to define the
11
+ * flow. The SDK does not yet expose a public executor because authored-body
12
+ * progress has no durable root journal. Internal lowering tests recover the
13
+ * definition here without making that seam a supported runner.
14
+ */
15
+ export function getAuthoredFlowDefinition<Input = unknown>(
16
+ handle: FlowHandle,
17
+ ): AuthoredFlowDefinition<Input> {
18
+ return getFlowDefinition<Input>(handle);
19
+ }
20
+
21
+ export type { AuthoredFlowDefinition, FlowHandle };
@@ -0,0 +1,196 @@
1
+ import { createHook, executionAsyncId, type AsyncHook } from 'node:async_hooks';
2
+
3
+ /**
4
+ * The promise graph an authored flow body actually creates.
5
+ *
6
+ * Three properties matter and each one is a repair of a measured defect:
7
+ *
8
+ * 1. **Scoped.** Only promises created while the owning lifecycle is the active
9
+ * `AsyncLocalStorage` store are recorded. A previous revision recorded every
10
+ * promise created anywhere in the process for the life of the flow; a probe
11
+ * measured 20 002 unrelated promises retained.
12
+ * 2. **Eagerly attributed.** A promise is attributed to a root the moment it is
13
+ * created, or the moment its parent becomes attributed — never by walking the
14
+ * graph per candidate at completion time. The walk made the completion gate
15
+ * quadratic: a 27 ms body with 30 000 ordinary awaits spent 95.8 s in the gate.
16
+ * 3. **Complete, not sampled.** `pending` is the set of tracked promises that have
17
+ * not settled. Derived work in flight is a fact the gate can read, so the gate
18
+ * never has to guess whether a failure "has landed yet".
19
+ *
20
+ * Attribution flows forward along the same `trigger` edge the previous revision
21
+ * walked backward, so it admits exactly the same set — it just knows the answer
22
+ * before it is asked. Children created before their parent becomes a root are
23
+ * parked in `unattributedChildren` and flooded when the root is registered; a
24
+ * promise cannot settle before its ancestors, and a root is always registered
25
+ * before it settles, so no parked child is ever missed.
26
+ */
27
+ export class AuthoredPromiseGraph {
28
+ private readonly hook: AsyncHook;
29
+ private readonly triggers = new Map<number, number>();
30
+ private readonly creationContexts = new Map<number, number>();
31
+ private readonly resolutionCauses = new Map<number, number>();
32
+ private readonly promiseIds = new WeakMap<object, number>();
33
+ private readonly handles = new Map<number, Promise<unknown>>();
34
+ private readonly unattributedChildren = new Map<number, number[]>();
35
+ private readonly attributedRoots = new Map<number, number>();
36
+ private readonly roots = new Set<number>();
37
+ private readonly pending = new Set<number>();
38
+ private dependencyCache: { readonly start: number; readonly found: ReadonlySet<number> } | undefined;
39
+
40
+ constructor(
41
+ private readonly inScope: () => boolean,
42
+ private readonly onPromiseResolve: (asyncId: number) => void,
43
+ ) {
44
+ this.hook = createHook({
45
+ init: (asyncId, type, triggerAsyncId, resource) => {
46
+ if (type !== 'PROMISE' || typeof resource !== 'object' || resource === null) return;
47
+ if (!this.inScope()) return;
48
+ this.triggers.set(asyncId, triggerAsyncId);
49
+ this.creationContexts.set(asyncId, executionAsyncId());
50
+ this.promiseIds.set(resource, asyncId);
51
+ this.handles.set(asyncId, resource as Promise<unknown>);
52
+ this.pending.add(asyncId);
53
+ const root = this.roots.has(triggerAsyncId)
54
+ ? triggerAsyncId
55
+ : this.attributedRoots.get(triggerAsyncId);
56
+ if (root !== undefined) this.attribute(asyncId, root);
57
+ else if (this.triggers.has(triggerAsyncId)) this.park(triggerAsyncId, asyncId);
58
+ },
59
+ promiseResolve: (asyncId) => {
60
+ this.onPromiseResolve(asyncId);
61
+ if (!this.triggers.has(asyncId)) return;
62
+ const cause = executionAsyncId();
63
+ if (cause !== asyncId) this.resolutionCauses.set(asyncId, cause);
64
+ this.pending.delete(asyncId);
65
+ if (this.attributedRoots.has(asyncId) || this.roots.has(asyncId)) return;
66
+ // A combinator's aggregate is resolved from inside the reaction of one of
67
+ // its members, so it is not downstream of any member by `trigger` and can
68
+ // only inherit attribution here, from the context that resolved it. Without
69
+ // this, only `Promise.all` was covered — because it is the one combinator
70
+ // registered explicitly — and `Promise.allSettled`, `Promise.any` and
71
+ // `Promise.race` each carried a deferred derived failure to terminal
72
+ // success. Measured; see ops/probes/pr134-repair-0903/combinators.mjs.
73
+ const adopted = this.attributedRoots.get(cause);
74
+ if (adopted !== undefined) {
75
+ this.attribute(asyncId, adopted);
76
+ return;
77
+ }
78
+ // A settled, unattributed promise can never become attributed: attribution
79
+ // only reaches a promise while its root is still pending. Release it.
80
+ this.handles.delete(asyncId);
81
+ this.unattributedChildren.delete(asyncId);
82
+ },
83
+ });
84
+ }
85
+
86
+ enable(): void {
87
+ this.hook.enable();
88
+ }
89
+
90
+ disable(): void {
91
+ this.hook.disable();
92
+ }
93
+
94
+ idOf(value: object): number | undefined {
95
+ return this.promiseIds.get(value);
96
+ }
97
+
98
+ /** Mark a promise whose descendants belong to an authored operation. */
99
+ registerRoot(asyncId: number): void {
100
+ if (asyncId <= 0 || this.roots.has(asyncId)) return;
101
+ this.roots.add(asyncId);
102
+ const parked = this.unattributedChildren.get(asyncId);
103
+ if (parked === undefined) return;
104
+ this.unattributedChildren.delete(asyncId);
105
+ for (const child of parked) this.attribute(child, asyncId);
106
+ }
107
+
108
+ /** Promises derived from `roots` that have not settled. */
109
+ inFlightFrom(roots: ReadonlySet<number>): number[] {
110
+ const found: number[] = [];
111
+ for (const asyncId of this.pending) {
112
+ const root = this.attributedRoots.get(asyncId);
113
+ if (root !== undefined && roots.has(root)) found.push(asyncId);
114
+ }
115
+ return found;
116
+ }
117
+
118
+ /** Settled promises derived from `roots`, with their handles. */
119
+ settledFrom(roots: ReadonlySet<number>): Promise<unknown>[] {
120
+ const found: Promise<unknown>[] = [];
121
+ for (const [asyncId, root] of this.attributedRoots) {
122
+ if (!roots.has(root) || this.pending.has(asyncId)) continue;
123
+ const handle = this.handles.get(asyncId);
124
+ if (handle !== undefined) found.push(handle);
125
+ }
126
+ return found;
127
+ }
128
+
129
+ dependsOn(descendant: number, ancestor: number): boolean {
130
+ return this.dependenciesOf(descendant).has(ancestor);
131
+ }
132
+
133
+ clear(): void {
134
+ this.triggers.clear();
135
+ this.creationContexts.clear();
136
+ this.resolutionCauses.clear();
137
+ this.handles.clear();
138
+ this.unattributedChildren.clear();
139
+ this.attributedRoots.clear();
140
+ this.roots.clear();
141
+ this.pending.clear();
142
+ this.dependencyCache = undefined;
143
+ }
144
+
145
+ private attribute(start: number, root: number): void {
146
+ const stack = [start];
147
+ while (stack.length > 0) {
148
+ const current = stack.pop()!;
149
+ if (this.attributedRoots.has(current) || this.roots.has(current)) continue;
150
+ this.attributedRoots.set(current, root);
151
+ const parked = this.unattributedChildren.get(current);
152
+ if (parked === undefined) continue;
153
+ this.unattributedChildren.delete(current);
154
+ for (const child of parked) stack.push(child);
155
+ }
156
+ }
157
+
158
+ private park(parent: number, child: number): void {
159
+ const parked = this.unattributedChildren.get(parent);
160
+ if (parked === undefined) this.unattributedChildren.set(parent, [child]);
161
+ else parked.push(child);
162
+ }
163
+
164
+ /**
165
+ * Everything the promise identified by `start` could have waited for.
166
+ *
167
+ * Three edges, and the third is the repair. `trigger` and `resolutionCause`
168
+ * alone do not connect an async function's resumption context to the context
169
+ * it was suspended from, so a walk from `done()` reached only the *last*
170
+ * await's lineage: `const steps = [f.run(a), f.run(b)]; for (const s of steps)
171
+ * await s;` reported run-1 unawaited even though every step was awaited. The
172
+ * init-time `executionAsyncId()` — the context a promise was created in — is
173
+ * that missing edge, and it is a fact the runtime reports, not a widened
174
+ * approximation.
175
+ */
176
+ private dependenciesOf(start: number): ReadonlySet<number> {
177
+ const cached = this.dependencyCache;
178
+ if (cached !== undefined && cached.start === start) return cached.found;
179
+ const found = new Set<number>();
180
+ const stack = [start];
181
+ while (stack.length > 0) {
182
+ const current = stack.pop()!;
183
+ if (found.has(current)) continue;
184
+ found.add(current);
185
+ for (const edge of [
186
+ this.triggers.get(current),
187
+ this.resolutionCauses.get(current),
188
+ this.creationContexts.get(current),
189
+ ]) {
190
+ if (edge !== undefined && edge !== current) stack.push(edge);
191
+ }
192
+ }
193
+ this.dependencyCache = { start, found };
194
+ return found;
195
+ }
196
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Gate-3 foundation: choose the next work package from ops/BACKLOG.md.
3
+ *
4
+ * The selection rule lived only inside a shell one-liner in
5
+ * testdata/backlog-picker.flow.yaml, which made the flow's central claim —
6
+ * that selection is DETERMINISTIC — impossible to assert. Review caught the
7
+ * missing test (PR #20, P1). A rule that cannot be tested is a rule nobody can
8
+ * rely on, so it lives here and the flow calls it.
9
+ *
10
+ * The rule: the first top-level bullet whose title is bold. Deliberately dull.
11
+ * A Garden that proposes its own work must be predictable before it is clever
12
+ * — if two runs over identical input can disagree, nothing downstream can
13
+ * reason about what the system decided or why.
14
+ */
15
+
16
+ /** First bold top-level bullet: `- **Title** rest`. */
17
+ const ENTRY = /^- \*\*(.+?)\*\*\s*(.*(?:\n .*)*)/m;
18
+ const CODE_REFERENCE = /`([^`]+)`/g;
19
+ // Outcomes include explicit changes and concrete defect statements. A list of
20
+ // identifiers or links alone carries neither, so it cannot qualify as work.
21
+ const ENGINEERING_OUTCOME =
22
+ /\b(?:add|asserts?|breaks?|build|cannot|capture|catches?|change|close|collapses?|cross-compile|delete|document|drift|duplicates?|fails?|fix|implement|invoke|leaks?|make|missing|must|needs?|no (?:end-to-end )?coverage|persist|refuse|register|replace|reserve|restore|run|scope|should|spawn|untested|update|use|validate|verified?|wire|wrong)\b/i;
23
+
24
+ export interface BacklogEntry {
25
+ title: string;
26
+ body: string;
27
+ }
28
+
29
+ export interface ValidatedWorkPackage {
30
+ title: string;
31
+ files_in_scope: string[];
32
+ definition_of_done: string[];
33
+ description?: string;
34
+ gate?: number | null;
35
+ }
36
+
37
+ export type WorkPackageValidationReason =
38
+ | 'missing_title'
39
+ | 'unterminated_backticks'
40
+ | 'missing_scope'
41
+ | 'missing_definition_of_done';
42
+
43
+ export type WorkPackageValidation =
44
+ | { accepted: true; work: ValidatedWorkPackage }
45
+ | { accepted: false; reason: WorkPackageValidationReason };
46
+
47
+ /**
48
+ * Returns the selected entry, or null when the backlog holds no actionable
49
+ * one. Null is a real answer — "nothing to do" — not a failure.
50
+ */
51
+ export function selectBacklogEntry(markdown: string): BacklogEntry | null {
52
+ const match = ENTRY.exec(markdown);
53
+ if (!match) return null;
54
+ return { title: match[1] ?? '', body: (match[2] ?? '').trim() };
55
+ }
56
+
57
+ /** Render the selected entry as a work package. */
58
+ export function renderWorkPackage(entry: BacklogEntry): string {
59
+ return [
60
+ `# NEXT — ${entry.title}`,
61
+ '',
62
+ 'Selected from ops/BACKLOG.md by the backlog picker (gate 3).',
63
+ 'Selection rule: the first top-level bullet whose title is bold.',
64
+ '',
65
+ '## Scope',
66
+ '',
67
+ entry.body || '(the backlog entry carried no detail beyond its title)',
68
+ '',
69
+ '## Definition of done',
70
+ '',
71
+ 'Restate the entry as passing commands before building against it. An entry',
72
+ 'that cannot be turned into a command is not yet a work package.',
73
+ '',
74
+ ].join('\n');
75
+ }
76
+
77
+ /** Accept a complete emitted package, or name the first missing requirement. */
78
+ export function validateWorkPackage(input: unknown): WorkPackageValidation {
79
+ if (!isRecord(input) || !isNonEmptyString(input['title'])) {
80
+ return { accepted: false, reason: 'missing_title' };
81
+ }
82
+ if (hasUnterminatedBackticks(input)) {
83
+ return { accepted: false, reason: 'unterminated_backticks' };
84
+ }
85
+ if (!isNonEmptyStringArray(input['files_in_scope'])) {
86
+ return { accepted: false, reason: 'missing_scope' };
87
+ }
88
+ if (!isNonEmptyStringArray(input['definition_of_done'])) {
89
+ return { accepted: false, reason: 'missing_definition_of_done' };
90
+ }
91
+ return { accepted: true, work: input as unknown as ValidatedWorkPackage };
92
+ }
93
+
94
+ /** Whether the backlog text used to derive a package has an unmatched backtick. */
95
+ export function hasUnterminatedBackticks(input: Record<string, unknown>): boolean {
96
+ const text = [input['title'], input['description']]
97
+ .filter((value): value is string => typeof value === 'string')
98
+ .join(' ');
99
+ return (text.match(/`/g)?.length ?? 0) % 2 === 1;
100
+ }
101
+
102
+ function isRecord(value: unknown): value is Record<string, unknown> {
103
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
104
+ }
105
+
106
+ function isNonEmptyString(value: unknown): value is string {
107
+ return typeof value === 'string' && value.trim().length > 0;
108
+ }
109
+
110
+ function isNonEmptyStringArray(value: unknown): value is string[] {
111
+ return Array.isArray(value) && value.length > 0 && value.every(isNonEmptyString);
112
+ }
113
+
114
+ /**
115
+ * Build a work package from a backlog entry.
116
+ *
117
+ * This lives in the SDK because two flow steps need it: `select-entry` has to
118
+ * build a candidate package to know whether an entry is actionable at all, and
119
+ * `emit-package` has to build the package it emits. When the logic was inlined
120
+ * in both, the two could drift silently — the flow would select an entry on one
121
+ * rule and describe it by another.
122
+ */
123
+ export function packageFromEntry(entry: BacklogEntry): Record<string, unknown> {
124
+ const blob = `${entry.title} ${entry.body}`;
125
+ const references = [...entry.body.matchAll(CODE_REFERENCE)].map((match) => match[1] ?? '');
126
+ const files = [
127
+ ...new Set(
128
+ references
129
+ .filter(
130
+ (candidate): candidate is string =>
131
+ /^[A-Za-z_][A-Za-z0-9._-]*(?:\/[A-Za-z0-9._-]*)+$/.test(candidate) &&
132
+ !/^\/|\/\//.test(candidate),
133
+ ),
134
+ ),
135
+ ];
136
+ const hasEngineeringOutcome = ENGINEERING_OUTCOME.test(entry.body);
137
+ // A symbol or command locates work in this repository, but not necessarily
138
+ // one file. Preserve that honest breadth instead of discarding the signal.
139
+ if (files.length === 0 && references.length > 0 && hasEngineeringOutcome) {
140
+ files.push('.');
141
+ }
142
+ const gate = blob.match(/\bgate[ -]?(\d+)\b/i);
143
+ const definitionOfDone = hasEngineeringOutcome
144
+ ? [entry.title.replace(/[.:]\s*$/, '')]
145
+ : [];
146
+ return {
147
+ title: entry.title,
148
+ description: entry.body,
149
+ files_in_scope: files,
150
+ gate: gate ? Number(gate[1]) : null,
151
+ definition_of_done: definitionOfDone,
152
+ };
153
+ }