@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,132 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { pollHackerNewsOnce, type EventSink } from './hn-poller.js';
5
+ import { JournalClient } from './journal-client.js';
6
+ import type { EventSubmitResult } from './protocol.js';
7
+
8
+ const sdkRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
9
+ const repositoryRoot = resolve(sdkRoot, '..');
10
+ const dataDir = resolve(process.env.RELAYFLOW_DATA_DIR ?? join(repositoryRoot, '.relayflowd'));
11
+ const socketPath = join(dataDir, 'relayflowd.sock');
12
+ const specPath = join(repositoryRoot, 'testdata', 'hn-monitor.spec.canonical.json');
13
+
14
+ interface Submission {
15
+ storyId: unknown;
16
+ outcome: EventSubmitResult;
17
+ }
18
+
19
+ async function main(): Promise<void> {
20
+ const spec: unknown = JSON.parse(await readFile(specPath, 'utf8'));
21
+ const client = new JournalClient(socketPath);
22
+
23
+ try {
24
+ await client.connect();
25
+ } catch (error) {
26
+ throw new Error(
27
+ `No relayflowd is listening at "${socketPath}". Start it with: relayflowd --data-dir "${dataDir}" serve`,
28
+ { cause: error },
29
+ );
30
+ }
31
+
32
+ try {
33
+ await client.hello('hn-monitor-demo');
34
+ const submissions: Submission[] = [];
35
+ const sink: EventSink = {
36
+ async eventSubmit(submittedSpec, event) {
37
+ const outcome = await client.eventSubmit(submittedSpec, event);
38
+ submissions.push({ storyId: storyId(event.payload), outcome });
39
+ return outcome;
40
+ },
41
+ };
42
+
43
+ console.log('Fetching live Hacker News top stories...');
44
+ await pollHackerNewsOnce(spec, sink);
45
+
46
+ let woke = 0;
47
+ const createdRunIds: string[] = [];
48
+ for (const { storyId, outcome } of submissions) {
49
+ const wake = outcome.run === undefined || outcome.run === null ? 'none' : 'created';
50
+ if (wake === 'created') {
51
+ woke += 1;
52
+ const runId = typeof outcome.run === 'string' ? outcome.run : (outcome.run as { run_id?: string })?.run_id;
53
+ if (runId) createdRunIds.push(runId);
54
+ }
55
+ console.log(
56
+ `Story ${String(storyId)}: matched=${outcome.matched} deduped=${outcome.deduped} wake=${wake}`,
57
+ );
58
+ }
59
+
60
+ // Say exactly what was proven, and no more.
61
+ //
62
+ // A wake is a run being CREATED. It is not the flow's steps being
63
+ // EXECUTED — that needs an agent worker attached to the kernel, and
64
+ // `relayflowd serve` alone does not provide one. Review caught this
65
+ // (PR #19, P1): the first version of this demo printed created wakes and
66
+ // let the reader conclude a workload had run. It had not.
67
+ //
68
+ // The distinction is the whole of RFC-0001 §3 gate 2: "a real proactive
69
+ // workload RUNS as a relayflow" is a claim about execution, not about
70
+ // enqueueing.
71
+ console.log('');
72
+ if (woke > 0) {
73
+ console.log(`PROVEN: ${woke} run(s) created from live Hacker News data via event.submit.`);
74
+ console.log(' The event path — fetch, match, dedupe claim, wake — works end to end.');
75
+ console.log('');
76
+
77
+ // Do not ASSERT that nothing executed — ask the kernel and report what it
78
+ // says. The first version of this block hardcoded "no agent worker is
79
+ // attached", which would have been a false statement the moment someone
80
+ // attached one. Evidence is captured, not narrated.
81
+ const observed = createdRunIds[0];
82
+ if (observed === undefined) {
83
+ console.log('NOT PROVEN: that those runs EXECUTED — no run id came back to inspect.');
84
+ } else {
85
+ const snapshot = await client.runGet(observed);
86
+ const steps = Object.entries(snapshot.steps);
87
+ // A run nobody works sits in `runnable` — ready, with no worker to
88
+ // claim it. So "not pending" is NOT evidence of execution; only a step
89
+ // that reached `running` or `done` proves a worker picked it up.
90
+ const executed = steps.filter(([, step]) => step.state === 'running' || step.state === 'done').length;
91
+ const stateCounts = steps.map(([id, step]) => `${id}=${step.state}`).join(' ');
92
+ console.log(`Observed run ${observed}: status=${snapshot.status}, steps: ${stateCounts}`);
93
+ if (executed === 0) {
94
+ console.log('');
95
+ console.log('NOT PROVEN: that those runs EXECUTED. No step reached running or done,');
96
+ console.log(' which is what a created-but-unworked run looks like: `relayflowd serve`');
97
+ console.log(' alone attaches no agent worker. Gate 2 asks whether a workload RUNS');
98
+ console.log(' as a relayflow; this shows it is woken, not that it ran.');
99
+ // Ordering matters, and the obvious advice is wrong. A run that
100
+ // finds no worker parks; attaching one AFTERWARDS does not re-drive
101
+ // it, because nothing revisits parked runs. Measured on the live
102
+ // kernel: attach-then-submit dispatches, submit-then-attach does not
103
+ // until run.resume is called.
104
+ console.log(' To close it, the worker must be attached BEFORE these events are');
105
+ console.log(' submitted — attaching afterwards does not re-drive a parked run.');
106
+ console.log(' Already parked? Call run.resume on it once a worker is attached.');
107
+ } else {
108
+ console.log('');
109
+ console.log(`ALSO PROVEN: execution happened — ${String(executed)} step(s) reached`);
110
+ console.log(' running or done, so a worker claimed this run. That is gate 2 proper.');
111
+ }
112
+ }
113
+ } else {
114
+ console.log('No runs were created. Either every story was already claimed (dedupe working');
115
+ console.log('as intended on a repeat poll), or nothing matched the subscription.');
116
+ }
117
+ } finally {
118
+ client.close();
119
+ }
120
+ }
121
+
122
+ function storyId(payload: unknown): unknown {
123
+ if (typeof payload === 'object' && payload !== null && 'id' in payload) {
124
+ return payload.id;
125
+ }
126
+ return 'unknown';
127
+ }
128
+
129
+ main().catch((error: unknown) => {
130
+ console.error(error instanceof Error ? error.message : String(error));
131
+ process.exitCode = 1;
132
+ });
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Directory watcher -> relayflow events.
3
+ *
4
+ * Second proactive workload on gate 2 primitives (hn-monitor is the first).
5
+ * Deliberately non-provider: no HTTP, no API tokens, no gate-6 dependency —
6
+ * just a directory poll. This proves the runner pattern generalizes beyond
7
+ * `hn-poller` without regressing RFC-0001 §6 (which assigns providers to
8
+ * relayfile adapters, not SDK code).
9
+ *
10
+ * How it works: each poll lists the target directory, dedupes against a
11
+ * caller-supplied `seen` set (or an internal Map if none provided), and
12
+ * submits a `dir.file_appeared` event for each unseen entry through the
13
+ * journal protocol. The kernel then dispatches the flow's agent step for
14
+ * each new file.
15
+ *
16
+ * Deduplication is still ultimately the kernel's job (flow's
17
+ * `dedupeKeyTemplate` + the (flow, subscription, key) claim). This layer's
18
+ * `seen` set is a cheap pre-filter so we don't spam `event.submit` with the
19
+ * same paths on every poll — an optimization, not a correctness contract.
20
+ *
21
+ * Real-world analog: an "inbox" directory that a human or another system
22
+ * drops files into, triggering a per-file flow (summarize, ingest, route,
23
+ * whatever the step declares).
24
+ */
25
+
26
+ import { promises as fsp } from 'node:fs';
27
+ import { join } from 'node:path';
28
+
29
+ /** Anything that can submit an event through the journal protocol. */
30
+ export interface EventSink {
31
+ eventSubmit(spec: unknown, event: { type: string; payload?: unknown; key?: string }): Promise<unknown>;
32
+ }
33
+
34
+ /** Injected so I/O stays deterministic in tests. */
35
+ export interface DirLister {
36
+ (dir: string): Promise<Array<{ name: string; size: number; mtimeMs: number; isFile: boolean }>>;
37
+ }
38
+
39
+ const defaultLister: DirLister = async (dir) => {
40
+ const entries = await fsp.readdir(dir, { withFileTypes: true });
41
+ const out: Array<{ name: string; size: number; mtimeMs: number; isFile: boolean }> = [];
42
+ for (const ent of entries) {
43
+ if (!ent.isFile()) continue;
44
+ const full = join(dir, ent.name);
45
+ const stat = await fsp.stat(full);
46
+ out.push({
47
+ name: ent.name,
48
+ size: stat.size,
49
+ mtimeMs: stat.mtimeMs,
50
+ isFile: true,
51
+ });
52
+ }
53
+ return out;
54
+ };
55
+
56
+ export interface PollOptions {
57
+ /** Directory to watch. Required. */
58
+ dir: string;
59
+ /**
60
+ * Set of relative paths already seen. The poller mutates it, adding each
61
+ * new file it submits. Callers persist this across polls to avoid
62
+ * re-submitting; internal callers can pass a fresh Set each poll if
63
+ * they'd rather rely on the kernel's dedupe claim.
64
+ */
65
+ seen: Set<string>;
66
+ /**
67
+ * Lister override — tests inject a deterministic fake. Production uses
68
+ * fs.readdir.
69
+ */
70
+ lister?: DirLister;
71
+ /**
72
+ * Cap on files per poll (safety valve against dropping thousands into
73
+ * the directory at once). Default 100.
74
+ */
75
+ fileLimit?: number;
76
+ }
77
+
78
+ const DEFAULT_FILE_LIMIT = 100;
79
+
80
+ /**
81
+ * List the directory once and submit a `dir.file_appeared` event for each
82
+ * unseen file. Adds each submitted path to `seen`.
83
+ *
84
+ * Returns the submit outcomes (one per new file). Journal errors from
85
+ * `eventSubmit` propagate; empty result is not an error; a missing
86
+ * directory throws (the caller decides whether that's a fetch error or
87
+ * a real failure — the runner classifies).
88
+ */
89
+ export async function pollDirectoryOnce(
90
+ spec: unknown,
91
+ sink: EventSink,
92
+ options: PollOptions,
93
+ ): Promise<unknown[]> {
94
+ const lister = options.lister ?? defaultLister;
95
+ const fileLimit = options.fileLimit ?? DEFAULT_FILE_LIMIT;
96
+ const seen = options.seen;
97
+
98
+ const entries = await lister(options.dir);
99
+ const fresh = entries
100
+ .filter((e) => e.isFile && !seen.has(e.name))
101
+ .slice(0, fileLimit);
102
+
103
+ const outcomes: unknown[] = [];
104
+ for (const entry of fresh) {
105
+ outcomes.push(
106
+ await sink.eventSubmit(spec, {
107
+ type: 'dir.file_appeared',
108
+ payload: {
109
+ type: 'file',
110
+ path: entry.name,
111
+ size: entry.size,
112
+ mtime_ms: entry.mtimeMs,
113
+ },
114
+ }),
115
+ );
116
+ // Only add to `seen` AFTER a successful submit — a journal failure
117
+ // means the event didn't reach the kernel, so the next poll should
118
+ // retry submission.
119
+ seen.add(entry.name);
120
+ }
121
+ return outcomes;
122
+ }
@@ -0,0 +1,69 @@
1
+ import { readFileSync, statSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import type { CheckFailureKind } from './failure-kinds.js';
4
+
5
+ export const MAX_DIRECT_INPUT_BYTES = 1_048_576;
6
+
7
+ export class DirectInputError extends Error {
8
+ constructor(readonly kind: CheckFailureKind, message: string) {
9
+ super(message);
10
+ this.name = 'DirectInputError';
11
+ }
12
+ }
13
+
14
+ /** Parse a direct-run input as an existing JSON file, otherwise as inline JSON. */
15
+ export function parseDirectInput(argument: string | undefined): unknown {
16
+ if (argument === undefined) {
17
+ throw new DirectInputError(
18
+ 'input_missing',
19
+ 'A directly run .flow.ts requires --input <inline-json-or-file>.',
20
+ );
21
+ }
22
+
23
+ const inputPath = resolve(argument);
24
+ let source = argument;
25
+ let fromFile = false;
26
+ try {
27
+ const stat = statSync(inputPath);
28
+ if (!stat.isFile()) {
29
+ throw new DirectInputError('input_unreadable', `Input file "${argument}" is not a regular file.`);
30
+ }
31
+ if (stat.size > MAX_DIRECT_INPUT_BYTES) {
32
+ throw tooLarge(argument, true);
33
+ }
34
+ try {
35
+ source = readFileSync(inputPath, 'utf8');
36
+ fromFile = true;
37
+ } catch {
38
+ throw new DirectInputError('input_unreadable', `Input file "${argument}" is not readable.`);
39
+ }
40
+ } catch (error) {
41
+ if (error instanceof DirectInputError) throw error;
42
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
43
+ throw new DirectInputError('input_unreadable', `Input file "${argument}" could not be inspected.`);
44
+ }
45
+ }
46
+
47
+ if (!fromFile && Buffer.byteLength(source, 'utf8') > MAX_DIRECT_INPUT_BYTES) {
48
+ throw tooLarge(argument, false);
49
+ }
50
+
51
+ try {
52
+ return JSON.parse(source) as unknown;
53
+ } catch {
54
+ const sourceKind = fromFile ? `Input file "${argument}"` : 'Inline input';
55
+ throw new DirectInputError('input_invalid', `${sourceKind} is not valid JSON.`);
56
+ }
57
+ }
58
+
59
+ function tooLarge(argument: string, fromFile: boolean): DirectInputError {
60
+ const sourceKind = fromFile ? `Input file "${argument}"` : 'Inline input';
61
+ return new DirectInputError(
62
+ 'input_too_large',
63
+ `${sourceKind} exceeds the ${MAX_DIRECT_INPUT_BYTES}-byte direct input limit.`,
64
+ );
65
+ }
66
+
67
+ export function isAuthoredFlowPath(path: string): boolean {
68
+ return /\.flow\.(?:ts|mts|js|mjs)$/.test(path);
69
+ }
@@ -0,0 +1,77 @@
1
+ const SHARED_SPEC_FAILURE_KINDS = ['invalid_spec'] as const;
2
+
3
+ /** Environment refusal kinds produced after spec validation succeeds. */
4
+ const PREFLIGHT_ENVIRONMENT_FAILURE_KINDS = [
5
+ 'cli_missing',
6
+ 'cli_unauthenticated',
7
+ 'cli_unresolved',
8
+ 'cli_unsupported',
9
+ 'command_missing',
10
+ 'model_unavailable',
11
+ 'model_unknown',
12
+ 'no_executor',
13
+ 'probe_failed',
14
+ ] as const;
15
+
16
+ /** Closed refusal taxonomy for public preflight (RFC covenant 2). */
17
+ export const PREFLIGHT_FAILURE_KINDS = [
18
+ ...SHARED_SPEC_FAILURE_KINDS,
19
+ ...PREFLIGHT_ENVIRONMENT_FAILURE_KINDS,
20
+ ] as const;
21
+
22
+ /** Input/command refusals emitted before the pure preflight predicates run. */
23
+ export const CHECK_INPUT_FAILURE_KINDS = [
24
+ 'config_invalid',
25
+ 'input_invalid',
26
+ 'input_missing',
27
+ 'input_too_large',
28
+ 'input_unreadable',
29
+ 'invalid_invocation',
30
+ ...SHARED_SPEC_FAILURE_KINDS,
31
+ ] as const;
32
+
33
+ export const CHECK_FAILURE_KINDS = [
34
+ ...CHECK_INPUT_FAILURE_KINDS,
35
+ ...PREFLIGHT_ENVIRONMENT_FAILURE_KINDS,
36
+ ] as const;
37
+
38
+ /**
39
+ * Warnings never refuse. Covenant 2 asks preflight to refuse *or warn* on
40
+ * anything it cannot prove, so a deterministic step always leaves exactly one
41
+ * of these: its command resolved (effects still unknowable), it did not
42
+ * resolve, or it could not be probed at all. Silence is not one of the states.
43
+ *
44
+ * `vacuous_gate` is the same principle applied to a declared gate that judges
45
+ * nothing: `schema: {}` and `schema: true` are legal and accepted, but a gate
46
+ * accepting every output must not be reported as if it constrained one.
47
+ */
48
+ export const PREFLIGHT_WARNING_KINDS = [
49
+ 'unprovable_effects',
50
+ 'command_unresolved',
51
+ 'command_unprovable',
52
+ 'vacuous_gate',
53
+ ] as const;
54
+
55
+ /** Closed outcome taxonomy owned by the `flows run` / `flows resume` surface. */
56
+ export const RUN_FAILURE_KINDS = [
57
+ 'daemon_unreachable',
58
+ 'protocol_error',
59
+ 'run_parked',
60
+ 'run_unavailable',
61
+ ] as const;
62
+
63
+ export type PreflightFailureKind = (typeof PREFLIGHT_FAILURE_KINDS)[number];
64
+ export type CheckFailureKind = (typeof CHECK_FAILURE_KINDS)[number];
65
+ export type PreflightWarningKind = (typeof PREFLIGHT_WARNING_KINDS)[number];
66
+ export type RunFailureKind = (typeof RUN_FAILURE_KINDS)[number];
67
+
68
+ const CHECK_FAILURE_KIND_SET: ReadonlySet<string> = new Set(CHECK_FAILURE_KINDS);
69
+ const RUN_FAILURE_KIND_SET: ReadonlySet<string> = new Set(RUN_FAILURE_KINDS);
70
+
71
+ export function isCheckFailureKind(value: string): value is CheckFailureKind {
72
+ return CHECK_FAILURE_KIND_SET.has(value);
73
+ }
74
+
75
+ export function isRunFailureKind(value: string): value is RunFailureKind {
76
+ return RUN_FAILURE_KIND_SET.has(value);
77
+ }
@@ -0,0 +1,72 @@
1
+ import type { StepSpec } from './spec.js';
2
+
3
+ export type JournalGateCheck =
4
+ | 'completion'
5
+ | 'exit_code'
6
+ | 'output_contains'
7
+ | 'json_schema';
8
+
9
+ export interface DataGateClassification {
10
+ kind: 'data';
11
+ checks: JournalGateCheck[];
12
+ evaluator: 'kernel';
13
+ preflightable: true;
14
+ replayable: true;
15
+ }
16
+
17
+ export interface StepGateInspection extends DataGateClassification {
18
+ stepId: string;
19
+ /**
20
+ * Present only when the declared `json_schema` accepts every possible
21
+ * output. `{}` and `true` are legal JSON Schema and the kernel accepts both,
22
+ * so this is not a refusal — but a gate that judges nothing must not read
23
+ * identically to a gate that judges something.
24
+ */
25
+ acceptsAnyOutput?: true;
26
+ }
27
+
28
+ /** Annotation-only keywords: present, they still constrain no instance. */
29
+ const ANNOTATIONS: ReadonlySet<string> = new Set([
30
+ '$anchor',
31
+ '$comment',
32
+ '$defs',
33
+ '$dynamicAnchor',
34
+ '$id',
35
+ '$schema',
36
+ '$vocabulary',
37
+ 'default',
38
+ 'definitions',
39
+ 'deprecated',
40
+ 'description',
41
+ 'examples',
42
+ 'readOnly',
43
+ 'title',
44
+ 'writeOnly',
45
+ ]);
46
+
47
+ /** A schema that accepts every output: `true`, `{}`, or annotations only. */
48
+ export function acceptsAnyOutput(schema: unknown): boolean {
49
+ if (schema === true) return true;
50
+ if (typeof schema !== 'object' || schema === null || Array.isArray(schema)) return false;
51
+ return Object.keys(schema).every((key) => ANNOTATIONS.has(key));
52
+ }
53
+
54
+ /** Describe the exact named checks the existing kernel applies to a step. */
55
+ export function inspectStepGate(step: StepSpec): StepGateInspection {
56
+ const checks: JournalGateCheck[] = [];
57
+ if (step.type === 'deterministic') checks.push('exit_code');
58
+ if (step.verification?.type === 'output_contains') checks.push('output_contains');
59
+ if (step.verification?.type === 'json_schema') checks.push('json_schema');
60
+ if (checks.length === 0) checks.push('completion');
61
+ const vacuous = step.verification?.type === 'json_schema'
62
+ && acceptsAnyOutput(step.verification.schema);
63
+ return {
64
+ stepId: step.id,
65
+ kind: 'data',
66
+ checks,
67
+ evaluator: 'kernel',
68
+ preflightable: true,
69
+ replayable: true,
70
+ ...(vacuous ? { acceptsAnyOutput: true as const } : {}),
71
+ };
72
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Hacker News -> relayflow events.
3
+ *
4
+ * This lives OUTSIDE `kernel/` deliberately. An earlier version called Hacker
5
+ * News from `kernel/relayflowd` and review rejected it (PR #16, P1): a
6
+ * durable-execution kernel must not own provider-specific product logic or
7
+ * network I/O, or engine availability and dependencies become coupled to an
8
+ * external service. The kernel gained a `ureq` dependency purely to fetch a
9
+ * JSON feed — a clear sign the code was in the wrong place.
10
+ *
11
+ * So the adapter sits on the authoring surface and submits its events through
12
+ * the journal protocol (`event.submit`), which is the same path any other
13
+ * external producer would use. The kernel learns about Hacker News the way it
14
+ * learns about everything else: as an event.
15
+ */
16
+
17
+ const TOP_STORIES_URL = 'https://hacker-news.firebaseio.com/v0/topstories.json';
18
+ const DEFAULT_STORY_LIMIT = 5;
19
+
20
+ /** Anything that can submit an event through the journal protocol. */
21
+ export interface EventSink {
22
+ eventSubmit(spec: unknown, event: { type: string; payload?: unknown; key?: string }): Promise<unknown>;
23
+ }
24
+
25
+ /** Injected so parsing and submission stay deterministic in tests. */
26
+ export type Fetcher = (url: string) => Promise<string>;
27
+
28
+ /**
29
+ * Typed fetch-transport error. Callers (like sdk/src/cli/hn-monitor.ts)
30
+ * `instanceof` this to distinguish transient HN fetch failures from
31
+ * journal failures — avoids message-string matching (fragile cross-module
32
+ * coupling) and covers the shapes fetch() itself throws (network errors,
33
+ * ECONNREFUSED, TypeError('fetch failed')).
34
+ */
35
+ export class HnTransientFetchError extends Error {
36
+ constructor(message: string, cause?: unknown) {
37
+ // Use the native ErrorOptions.cause path so stack formatting and
38
+ // downstream inspectors (util.inspect, structured loggers) see it.
39
+ super(message, cause !== undefined ? { cause } : undefined);
40
+ this.name = 'HnTransientFetchError';
41
+ }
42
+ }
43
+
44
+ const defaultFetcher: Fetcher = async (url) => {
45
+ let response: Response;
46
+ try {
47
+ response = await fetch(url);
48
+ } catch (cause) {
49
+ // Network-layer failures from fetch() itself — TypeError('fetch failed'),
50
+ // ECONNREFUSED, DNS lookup failures — are all transient.
51
+ throw new HnTransientFetchError(`HN fetch failed: ${String(cause)}`, cause);
52
+ }
53
+ if (!response.ok) {
54
+ throw new HnTransientFetchError(`HN fetch failed: HTTP ${response.status}`);
55
+ }
56
+ return response.text();
57
+ };
58
+
59
+ export interface PollOptions {
60
+ storyLimit?: number;
61
+ fetcher?: Fetcher;
62
+ createdBy?: string;
63
+ }
64
+
65
+ /**
66
+ * Fetch the top-stories feed once and submit each story as an event.
67
+ *
68
+ * Dedupe is the kernel's job, not ours: the flow's `dedupeKeyTemplate` plus the
69
+ * (flow, subscription, key) claim means submitting the same story twice wakes
70
+ * it once. This function deliberately does not track what it has already seen.
71
+ */
72
+ export async function pollHackerNewsOnce(
73
+ spec: unknown,
74
+ sink: EventSink,
75
+ options: PollOptions = {},
76
+ ): Promise<unknown[]> {
77
+ const storyLimit = options.storyLimit ?? DEFAULT_STORY_LIMIT;
78
+ const fetcher = options.fetcher ?? defaultFetcher;
79
+
80
+ const body = await fetcher(TOP_STORIES_URL);
81
+
82
+ let storyIds: unknown;
83
+ try {
84
+ storyIds = JSON.parse(body);
85
+ } catch (cause) {
86
+ throw new HnTransientFetchError(`HN top stories response was not JSON: ${String(cause)}`);
87
+ }
88
+ if (!Array.isArray(storyIds)) {
89
+ throw new HnTransientFetchError('HN top stories response was not an array');
90
+ }
91
+
92
+ const outcomes: unknown[] = [];
93
+ for (const id of storyIds.slice(0, storyLimit)) {
94
+ outcomes.push(
95
+ await sink.eventSubmit(spec, {
96
+ type: 'hn.story_posted',
97
+ payload: { id, type: 'story' },
98
+ }),
99
+ );
100
+ }
101
+ return outcomes;
102
+ }
103
+
104
+ export const HN_TOP_STORIES_URL = TOP_STORIES_URL;