@hraness/wordcell 0.20.0-bootstrap.1

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 (215) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +749 -0
  3. package/dist/agent-context.js +35 -0
  4. package/dist/agent-guide-audit.js +18 -0
  5. package/dist/attachments.js +19 -0
  6. package/dist/authoring.js +33 -0
  7. package/dist/benchmark.js +19 -0
  8. package/dist/capture.js +49 -0
  9. package/dist/cli.js +57 -0
  10. package/dist/clip/acquire.js +57 -0
  11. package/dist/clip/args.js +21 -0
  12. package/dist/clip/bounded-byte-buffer.js +9 -0
  13. package/dist/clip/browser-profiles.js +17 -0
  14. package/dist/clip/bundle-reader.js +11 -0
  15. package/dist/clip/cli.js +27 -0
  16. package/dist/clip/cookies.js +21 -0
  17. package/dist/clip/defuddle-worker.js +84 -0
  18. package/dist/clip/doctor.js +32 -0
  19. package/dist/clip/jobs.js +784 -0
  20. package/dist/clip/network-proxy.js +9 -0
  21. package/dist/clip/network.js +28 -0
  22. package/dist/clip/persist.js +30 -0
  23. package/dist/clip/refresh.js +11 -0
  24. package/dist/clip/terminal.js +11 -0
  25. package/dist/evaluation-builder.js +12238 -0
  26. package/dist/evaluation-kb.js +26 -0
  27. package/dist/evaluation.js +35 -0
  28. package/dist/git.js +39 -0
  29. package/dist/graph.js +41 -0
  30. package/dist/index-054mb7d3.js +1974 -0
  31. package/dist/index-06c9ctr6.js +738 -0
  32. package/dist/index-180zbkkt.js +2480 -0
  33. package/dist/index-1gwbassd.js +743 -0
  34. package/dist/index-1xxnjn0d.js +171 -0
  35. package/dist/index-23z4zxgg.js +119 -0
  36. package/dist/index-2gv8y733.js +432 -0
  37. package/dist/index-3ehtt59c.js +199 -0
  38. package/dist/index-3rm7cz6h.js +373 -0
  39. package/dist/index-48pz4jpc.js +392 -0
  40. package/dist/index-4j3tt0c3.js +234 -0
  41. package/dist/index-5n05se68.js +307 -0
  42. package/dist/index-5vwpzb5a.js +764 -0
  43. package/dist/index-6jcz0m1c.js +712 -0
  44. package/dist/index-6sw24nvv.js +1189 -0
  45. package/dist/index-8a08v6x8.js +56 -0
  46. package/dist/index-adx6khj5.js +488 -0
  47. package/dist/index-b88v3vtm.js +805 -0
  48. package/dist/index-bcknqxrq.js +1943 -0
  49. package/dist/index-bs0yf4pp.js +711 -0
  50. package/dist/index-by0pbant.js +827 -0
  51. package/dist/index-d13v9ckt.js +150 -0
  52. package/dist/index-dfag79p7.js +370 -0
  53. package/dist/index-e4fagfjz.js +44 -0
  54. package/dist/index-e5fbsywq.js +658 -0
  55. package/dist/index-ekpwvbra.js +1070 -0
  56. package/dist/index-f984hw45.js +927 -0
  57. package/dist/index-gh719d91.js +63 -0
  58. package/dist/index-h170byqw.js +371 -0
  59. package/dist/index-hgve9rh2.js +2046 -0
  60. package/dist/index-hya40gb2.js +474 -0
  61. package/dist/index-j4zgmzjr.js +152 -0
  62. package/dist/index-m7et4rz4.js +966 -0
  63. package/dist/index-mxxxytys.js +1254 -0
  64. package/dist/index-n05s3wsb.js +2728 -0
  65. package/dist/index-nd6nynv2.js +1162 -0
  66. package/dist/index-npg9z1a4.js +431 -0
  67. package/dist/index-pj501bh1.js +140 -0
  68. package/dist/index-rdd5f7jj.js +70 -0
  69. package/dist/index-sy0zety7.js +3630 -0
  70. package/dist/index-tkpxp48t.js +1166 -0
  71. package/dist/index-w2zc0vwa.js +571 -0
  72. package/dist/index-why54q5p.js +462 -0
  73. package/dist/index-x3fthpsc.js +585 -0
  74. package/dist/index-x6m57nfc.js +1041 -0
  75. package/dist/index.js +784 -0
  76. package/dist/kb-alias.js +55 -0
  77. package/dist/navigation.js +13 -0
  78. package/dist/pdf.js +85 -0
  79. package/dist/percolate.js +44 -0
  80. package/dist/portfolio.js +77 -0
  81. package/dist/query.js +29 -0
  82. package/dist/repository-memory.js +60 -0
  83. package/dist/sdk.js +36 -0
  84. package/dist/search-rules.js +34 -0
  85. package/dist/search.js +22 -0
  86. package/dist/semantic.js +39 -0
  87. package/dist/source-inbox.js +17 -0
  88. package/dist/untrusted-content.js +32 -0
  89. package/dist/url-intelligence.js +74 -0
  90. package/dist/workflow.js +23 -0
  91. package/dist/workflows/decision-context.js +20 -0
  92. package/dist/workflows/explain-change.js +8 -0
  93. package/dist/workflows/index.js +28 -0
  94. package/dist/workflows/plan-radar.js +10 -0
  95. package/package.json +421 -0
  96. package/skills/wordcell/AGENTS.md +22 -0
  97. package/skills/wordcell/SKILL.md +91 -0
  98. package/skills/wordcell/agents/openai.yaml +4 -0
  99. package/skills/wordcell/references/companion-skills.md +96 -0
  100. package/skills/wordcell/references/customize.md +123 -0
  101. package/skills/wordcell/references/pdf-review.md +51 -0
  102. package/skills/wordcell/references/percolate.md +176 -0
  103. package/skills/wordcell/references/plan-structure.md +105 -0
  104. package/skills/wordcell/references/plan.md +115 -0
  105. package/skills/wordcell/references/query.md +252 -0
  106. package/skills/wordcell/references/refresh.md +133 -0
  107. package/skills/wordcell/references/save-pdf.md +125 -0
  108. package/skills/wordcell/references/save-url.md +178 -0
  109. package/skills/wordcell/references/url-authentication.md +75 -0
  110. package/skills/wordcell/references/url-platforms.md +25 -0
  111. package/skills/wordcell/templates/companion-skill.template.md +57 -0
  112. package/src/agent-context.ts +1241 -0
  113. package/src/agent-guide-audit.ts +750 -0
  114. package/src/attachments.ts +723 -0
  115. package/src/authoring-model.ts +798 -0
  116. package/src/authoring-platform.ts +549 -0
  117. package/src/authoring-program.ts +278 -0
  118. package/src/authoring-runtime.ts +54 -0
  119. package/src/authoring.ts +75 -0
  120. package/src/benchmark.ts +362 -0
  121. package/src/capture.ts +68 -0
  122. package/src/cli-program.ts +3662 -0
  123. package/src/cli.ts +6 -0
  124. package/src/clip/acquire.ts +1273 -0
  125. package/src/clip/archive-today.ts +402 -0
  126. package/src/clip/args.ts +423 -0
  127. package/src/clip/assets.ts +262 -0
  128. package/src/clip/bounded-byte-buffer.ts +75 -0
  129. package/src/clip/browser-profiles.ts +372 -0
  130. package/src/clip/bundle-reader.ts +608 -0
  131. package/src/clip/capture.ts +1054 -0
  132. package/src/clip/cli.ts +181 -0
  133. package/src/clip/cookies.ts +512 -0
  134. package/src/clip/doctor.ts +1110 -0
  135. package/src/clip/extract.ts +868 -0
  136. package/src/clip/jobs.ts +959 -0
  137. package/src/clip/lib.ts +861 -0
  138. package/src/clip/media.ts +1474 -0
  139. package/src/clip/metadata-search-tool/Cargo.lock +2498 -0
  140. package/src/clip/metadata-search-tool/Cargo.toml +18 -0
  141. package/src/clip/metadata-search-tool/runner.ts +685 -0
  142. package/src/clip/metadata-search-tool/src/main.rs +389 -0
  143. package/src/clip/metadata-search.ts +687 -0
  144. package/src/clip/network-proxy.ts +627 -0
  145. package/src/clip/network.ts +950 -0
  146. package/src/clip/package-root.ts +54 -0
  147. package/src/clip/persist.ts +1472 -0
  148. package/src/clip/platforms.ts +1824 -0
  149. package/src/clip/refresh.ts +197 -0
  150. package/src/clip/structured.ts +432 -0
  151. package/src/clip/terminal.ts +197 -0
  152. package/src/clip/url-intelligence.ts +581 -0
  153. package/src/clip/url-metadata-backfill.ts +616 -0
  154. package/src/clip/url-metadata-cli.ts +251 -0
  155. package/src/clip/url-metadata.ts +1397 -0
  156. package/src/evaluation-analysis-v2.ts +1440 -0
  157. package/src/evaluation-builder.ts +2539 -0
  158. package/src/evaluation-corpus-authoring.ts +2052 -0
  159. package/src/evaluation-evidence.ts +1147 -0
  160. package/src/evaluation-execution-v2.ts +426 -0
  161. package/src/evaluation-implementation.ts +131 -0
  162. package/src/evaluation-kb-closure.ts +1430 -0
  163. package/src/evaluation-kb-runner-v2.ts +735 -0
  164. package/src/evaluation-kb-v2.ts +2567 -0
  165. package/src/evaluation-kb.ts +1018 -0
  166. package/src/evaluation-measurement-v2.ts +1462 -0
  167. package/src/evaluation-packing-v2.ts +452 -0
  168. package/src/evaluation-redaction.ts +28 -0
  169. package/src/evaluation-v2.ts +3857 -0
  170. package/src/evaluation.ts +1220 -0
  171. package/src/git.ts +1276 -0
  172. package/src/graph.ts +1494 -0
  173. package/src/index.ts +21 -0
  174. package/src/init.ts +123 -0
  175. package/src/kb-alias.ts +14 -0
  176. package/src/navigation.ts +277 -0
  177. package/src/note-lock.ts +529 -0
  178. package/src/oh-adoption.ts +444 -0
  179. package/src/pdf/args.ts +202 -0
  180. package/src/pdf/capture.ts +298 -0
  181. package/src/pdf/cli.ts +159 -0
  182. package/src/pdf/extract.ts +303 -0
  183. package/src/pdf/layout.ts +618 -0
  184. package/src/pdf/markdown.ts +154 -0
  185. package/src/pdf/model.ts +234 -0
  186. package/src/pdf/ocr.ts +190 -0
  187. package/src/pdf/persist.ts +314 -0
  188. package/src/pdf/source.ts +135 -0
  189. package/src/pdf/tools.ts +146 -0
  190. package/src/pdf.ts +92 -0
  191. package/src/percolate.ts +1948 -0
  192. package/src/portfolio-audit.ts +574 -0
  193. package/src/portfolio-identity.ts +162 -0
  194. package/src/portfolio-registry.ts +473 -0
  195. package/src/portfolio.ts +765 -0
  196. package/src/query.ts +646 -0
  197. package/src/repository-memory.ts +1150 -0
  198. package/src/sdk.ts +1452 -0
  199. package/src/search-rules.ts +712 -0
  200. package/src/search.ts +710 -0
  201. package/src/semantic-runtime.ts +1139 -0
  202. package/src/semantic.ts +2480 -0
  203. package/src/source-inbox.ts +232 -0
  204. package/src/untrusted-content.ts +401 -0
  205. package/src/url-intelligence.ts +104 -0
  206. package/src/vault.ts +636 -0
  207. package/src/workflow-model.ts +444 -0
  208. package/src/workflow-platform.ts +74 -0
  209. package/src/workflow-program.ts +183 -0
  210. package/src/workflow-runtime.ts +51 -0
  211. package/src/workflow.ts +23 -0
  212. package/src/workflows/decision-context.ts +81 -0
  213. package/src/workflows/explain-change.ts +73 -0
  214. package/src/workflows/index.ts +3 -0
  215. package/src/workflows/plan-radar.ts +101 -0
@@ -0,0 +1,444 @@
1
+ import type { KnowledgeBaseSession } from "./sdk.js";
2
+
3
+ export const MAX_WORKFLOW_NODES = 64;
4
+ export const MAX_WORKFLOW_CONCURRENCY = 8;
5
+ export const MAX_GIT_WORKFLOW_CONCURRENCY = 4;
6
+ export const DEFAULT_WORKFLOW_OUTPUT_BYTES = 16 * 1_024 * 1_024;
7
+ export const MAX_WORKFLOW_OUTPUT_BYTES = 64 * 1_024 * 1_024;
8
+
9
+ const workflowIdPattern = /^[a-z][a-z0-9-]{0,63}$/u;
10
+
11
+ export type WorkflowResource = "default" | "git" | "qmd";
12
+
13
+ export type DefaultWorkflowResults = Readonly<Record<string, unknown>>;
14
+ export type WorkflowResultId<Results extends object> = Extract<keyof Results, string>;
15
+ export type WorkflowResultValue<Results extends object> = Results[WorkflowResultId<Results>];
16
+
17
+ export type WorkflowNodeContext<
18
+ Input,
19
+ KnowledgeBase,
20
+ Results extends object = DefaultWorkflowResults,
21
+ > = {
22
+ readonly input: Input;
23
+ readonly kb: KnowledgeBase;
24
+ readonly signal: AbortSignal;
25
+ readonly results: ReadonlyMap<WorkflowResultId<Results>, WorkflowResultValue<Results>>;
26
+ readonly result: <Id extends WorkflowResultId<Results>>(id: Id) => Results[Id];
27
+ };
28
+
29
+ export type WorkflowNode<
30
+ Input,
31
+ KnowledgeBase,
32
+ Results extends object = DefaultWorkflowResults,
33
+ > = {
34
+ readonly [Id in WorkflowResultId<Results>]: {
35
+ readonly id: Id;
36
+ readonly needs?: readonly WorkflowResultId<Results>[];
37
+ readonly resource?: WorkflowResource;
38
+ readonly run: (
39
+ context: WorkflowNodeContext<Input, KnowledgeBase, Results>,
40
+ ) => Results[Id] | Promise<Results[Id]>;
41
+ };
42
+ }[WorkflowResultId<Results>];
43
+
44
+ export type WorkflowDefinition<
45
+ Input,
46
+ KnowledgeBase,
47
+ Results extends object = DefaultWorkflowResults,
48
+ Output extends WorkflowResultId<Results> = WorkflowResultId<Results>,
49
+ > = {
50
+ readonly kind: "hraness-kb-workflow";
51
+ readonly version: 1;
52
+ readonly id: string;
53
+ readonly nodes: readonly WorkflowNode<Input, KnowledgeBase, Results>[];
54
+ readonly output: Output;
55
+ };
56
+
57
+ export type WorkflowSpecification<
58
+ Input,
59
+ KnowledgeBase,
60
+ Results extends object = DefaultWorkflowResults,
61
+ Output extends WorkflowResultId<Results> = WorkflowResultId<Results>,
62
+ > = Omit<
63
+ WorkflowDefinition<Input, KnowledgeBase, Results, Output>,
64
+ "kind" | "version"
65
+ >;
66
+
67
+ type WorkflowBuilderNode<
68
+ Input,
69
+ KnowledgeBase,
70
+ Results extends object,
71
+ Id extends string,
72
+ Needs extends readonly WorkflowResultId<Results>[],
73
+ Returned,
74
+ > = {
75
+ readonly id: Id & (Id extends WorkflowResultId<Results> ? never : unknown);
76
+ readonly needs?: Needs;
77
+ readonly resource?: WorkflowResource;
78
+ readonly run: (
79
+ context: WorkflowNodeContext<
80
+ Input,
81
+ KnowledgeBase,
82
+ Pick<Results, Needs[number]>
83
+ >,
84
+ ) => Returned;
85
+ };
86
+
87
+ export type WorkflowBuilder<
88
+ Input,
89
+ KnowledgeBase = KnowledgeBaseSession,
90
+ Results extends object = Readonly<Record<never, never>>,
91
+ > = {
92
+ readonly node: <
93
+ const Id extends string,
94
+ Returned,
95
+ const Needs extends readonly WorkflowResultId<Results>[] = readonly [],
96
+ >(
97
+ node: WorkflowBuilderNode<
98
+ Input,
99
+ KnowledgeBase,
100
+ Results,
101
+ Id,
102
+ Needs,
103
+ Returned
104
+ >,
105
+ ) => WorkflowBuilder<
106
+ Input,
107
+ KnowledgeBase,
108
+ Results & Readonly<Record<Id, Awaited<Returned>>>
109
+ >;
110
+ readonly output: <Output extends WorkflowResultId<Results>>(
111
+ output: Output,
112
+ ) => WorkflowDefinition<Input, KnowledgeBase, Results, Output>;
113
+ };
114
+
115
+ export type WorkflowNodeTrace = {
116
+ readonly id: string;
117
+ readonly needs: readonly string[];
118
+ readonly resource: WorkflowResource;
119
+ readonly status: "completed";
120
+ };
121
+
122
+ export type WorkflowRunResult<
123
+ Results extends object = DefaultWorkflowResults,
124
+ Output extends WorkflowResultId<Results> = WorkflowResultId<Results>,
125
+ > = {
126
+ readonly workflow: string;
127
+ readonly outputNode: Output;
128
+ readonly output: Results[Output];
129
+ /** Sum of each completed node result's structured-serialization bytes. */
130
+ readonly outputBytes: number;
131
+ readonly trace: readonly WorkflowNodeTrace[];
132
+ readonly results: ReadonlyMap<WorkflowResultId<Results>, WorkflowResultValue<Results>>;
133
+ };
134
+
135
+ export type WorkflowRunOptions<Input, KnowledgeBase> = {
136
+ readonly input: Input;
137
+ readonly kb: KnowledgeBase;
138
+ readonly concurrency?: number;
139
+ readonly resourceConcurrency?: Partial<Readonly<Record<WorkflowResource, number>>>;
140
+ /** Aggregate structured-serialization bytes across completed node results. */
141
+ readonly maxOutputBytes?: number;
142
+ readonly signal?: AbortSignal;
143
+ };
144
+
145
+ export type WorkflowFailureKind = "aborted" | "node-failed" | "output-limit";
146
+
147
+ export class WorkflowRunError extends Error {
148
+ readonly kind: WorkflowFailureKind;
149
+ readonly node?: string;
150
+
151
+ constructor(
152
+ kind: WorkflowFailureKind,
153
+ message: string,
154
+ options: ErrorOptions & { readonly node?: string } = {},
155
+ ) {
156
+ super(message, options);
157
+ this.name = "WorkflowRunError";
158
+ this.kind = kind;
159
+ if (options.node !== undefined) this.node = options.node;
160
+ }
161
+ }
162
+
163
+ function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
164
+ return typeof value === "object" && value !== null && !Array.isArray(value);
165
+ }
166
+
167
+ export function needsFor<Input, KnowledgeBase, Results extends object>(
168
+ node: WorkflowNode<Input, KnowledgeBase, Results>,
169
+ ): readonly WorkflowResultId<Results>[] {
170
+ return node.needs ?? [];
171
+ }
172
+
173
+ function validateWorkflowId(id: string, label: string): void {
174
+ if (!workflowIdPattern.test(id)) {
175
+ throw new Error(
176
+ `${label} must start with a lowercase letter and contain at most 64 lowercase letters, digits, or hyphens.`,
177
+ );
178
+ }
179
+ }
180
+
181
+ export function validateWorkflow<
182
+ Input,
183
+ KnowledgeBase,
184
+ Results extends object,
185
+ Output extends WorkflowResultId<Results>,
186
+ >(
187
+ definition: WorkflowDefinition<Input, KnowledgeBase, Results, Output>,
188
+ ): void {
189
+ validateWorkflowId(definition.id, "Workflow id");
190
+ if (definition.nodes.length < 1 || definition.nodes.length > MAX_WORKFLOW_NODES) {
191
+ throw new RangeError(
192
+ `A workflow must contain from 1 through ${MAX_WORKFLOW_NODES} nodes.`,
193
+ );
194
+ }
195
+ const nodesById = new Map<string, WorkflowNode<Input, KnowledgeBase, Results>>();
196
+ for (const node of definition.nodes) {
197
+ validateWorkflowId(node.id, "Workflow node id");
198
+ if (nodesById.has(node.id)) {
199
+ throw new Error(`Workflow node id ${JSON.stringify(node.id)} is duplicated.`);
200
+ }
201
+ if (typeof node.run !== "function") {
202
+ throw new Error(`Workflow node ${JSON.stringify(node.id)} must define run().`);
203
+ }
204
+ const needs = needsFor(node);
205
+ if (new Set(needs).size !== needs.length) {
206
+ throw new Error(
207
+ `Workflow node ${JSON.stringify(node.id)} repeats a dependency.`,
208
+ );
209
+ }
210
+ nodesById.set(node.id, node);
211
+ }
212
+ if (!nodesById.has(definition.output)) {
213
+ throw new Error(
214
+ `Workflow output node ${JSON.stringify(definition.output)} does not exist.`,
215
+ );
216
+ }
217
+ for (const node of definition.nodes) {
218
+ for (const dependency of needsFor(node)) {
219
+ if (!nodesById.has(dependency)) {
220
+ throw new Error(
221
+ `Workflow node ${JSON.stringify(node.id)} depends on unknown node ${JSON.stringify(dependency)}.`,
222
+ );
223
+ }
224
+ if (dependency === node.id) {
225
+ throw new Error(
226
+ `Workflow node ${JSON.stringify(node.id)} cannot depend on itself.`,
227
+ );
228
+ }
229
+ }
230
+ }
231
+
232
+ const remaining = new Set(nodesById.keys());
233
+ const completed = new Set<string>();
234
+ while (remaining.size > 0) {
235
+ const ready = definition.nodes.filter((node) =>
236
+ remaining.has(node.id)
237
+ && needsFor(node).every((dependency) => completed.has(dependency)));
238
+ if (ready.length === 0) {
239
+ throw new Error(
240
+ `Workflow contains a dependency cycle among: ${[...remaining].join(", ")}.`,
241
+ );
242
+ }
243
+ for (const node of ready) {
244
+ remaining.delete(node.id);
245
+ completed.add(node.id);
246
+ }
247
+ }
248
+ }
249
+
250
+ function createWorkflowBuilder<
251
+ Input,
252
+ KnowledgeBase,
253
+ Results extends object,
254
+ >(
255
+ id: string,
256
+ nodes: readonly WorkflowNode<Input, KnowledgeBase, Results>[],
257
+ ): WorkflowBuilder<Input, KnowledgeBase, Results> {
258
+ const builder: WorkflowBuilder<Input, KnowledgeBase, Results> = {
259
+ node: <
260
+ const Id extends string,
261
+ Returned,
262
+ const Needs extends readonly WorkflowResultId<Results>[] = readonly [],
263
+ >(
264
+ node: WorkflowBuilderNode<
265
+ Input,
266
+ KnowledgeBase,
267
+ Results,
268
+ Id,
269
+ Needs,
270
+ Returned
271
+ >,
272
+ ) => createWorkflowBuilder<
273
+ Input,
274
+ KnowledgeBase,
275
+ Results & Readonly<Record<Id, Awaited<Returned>>>
276
+ >(
277
+ id,
278
+ [...nodes, node] as unknown as readonly WorkflowNode<
279
+ Input,
280
+ KnowledgeBase,
281
+ Results & Readonly<Record<Id, Awaited<Returned>>>
282
+ >[],
283
+ ),
284
+ output: (output) => defineWorkflow({
285
+ id,
286
+ nodes,
287
+ output,
288
+ }),
289
+ };
290
+ return builder;
291
+ }
292
+
293
+ /** Define a finite trusted TypeScript workflow. Runtime validation is repeated at execution. */
294
+ export function defineWorkflow<
295
+ Input,
296
+ KnowledgeBase = KnowledgeBaseSession,
297
+ >(id: string): WorkflowBuilder<Input, KnowledgeBase>;
298
+ export function defineWorkflow<Input, KnowledgeBase>(
299
+ specification: WorkflowSpecification<Input, KnowledgeBase>,
300
+ ): WorkflowDefinition<Input, KnowledgeBase>;
301
+ export function defineWorkflow<
302
+ Input,
303
+ KnowledgeBase,
304
+ Results extends object,
305
+ Output extends WorkflowResultId<Results>,
306
+ >(
307
+ specification: WorkflowSpecification<Input, KnowledgeBase, Results, Output>,
308
+ ): WorkflowDefinition<Input, KnowledgeBase, Results, Output>;
309
+ export function defineWorkflow<
310
+ Input,
311
+ KnowledgeBase,
312
+ Results extends object,
313
+ Output extends WorkflowResultId<Results>,
314
+ >(
315
+ specification: string | WorkflowSpecification<
316
+ Input,
317
+ KnowledgeBase,
318
+ Results,
319
+ Output
320
+ >,
321
+ ):
322
+ | WorkflowDefinition<Input, KnowledgeBase, Results, Output>
323
+ | WorkflowBuilder<Input, KnowledgeBase> {
324
+ if (typeof specification === "string") {
325
+ return createWorkflowBuilder<Input, KnowledgeBase, Readonly<Record<never, never>>>(
326
+ specification,
327
+ [],
328
+ );
329
+ }
330
+ const definition: WorkflowDefinition<Input, KnowledgeBase, Results, Output> = {
331
+ kind: "hraness-kb-workflow",
332
+ version: 1,
333
+ ...specification,
334
+ };
335
+ validateWorkflow(definition);
336
+ return definition;
337
+ }
338
+
339
+ /** Validate a dynamically imported code-mode value before executing it. */
340
+ export function workflowFromUnknown(
341
+ value: unknown,
342
+ ): WorkflowDefinition<unknown, unknown> {
343
+ if (!isRecord(value)) throw new Error("Workflow export must be an object.");
344
+ if (value.kind !== "hraness-kb-workflow" || value.version !== 1) {
345
+ throw new Error("Workflow export has an unsupported kind or version.");
346
+ }
347
+ if (
348
+ typeof value.id !== "string"
349
+ || typeof value.output !== "string"
350
+ || !Array.isArray(value.nodes)
351
+ ) {
352
+ throw new Error("Workflow export has malformed id, output, or nodes.");
353
+ }
354
+ const nodes: WorkflowNode<unknown, unknown>[] = value.nodes.map((item, index) => {
355
+ if (!isRecord(item) || typeof item.id !== "string") {
356
+ throw new Error(`Workflow export node ${index} is malformed.`);
357
+ }
358
+ const run = item.run;
359
+ if (typeof run !== "function") {
360
+ throw new Error(`Workflow export node ${index} is malformed.`);
361
+ }
362
+ const needs = item.needs;
363
+ if (needs !== undefined && (!Array.isArray(needs) || needs.some((entry) => typeof entry !== "string"))) {
364
+ throw new Error(`Workflow export node ${index}.needs must be an array of strings.`);
365
+ }
366
+ const resource = item.resource;
367
+ if (resource !== undefined && resource !== "default" && resource !== "git" && resource !== "qmd") {
368
+ throw new Error(`Workflow export node ${index}.resource is invalid.`);
369
+ }
370
+ return {
371
+ id: item.id,
372
+ ...(needs === undefined ? {} : { needs }),
373
+ ...(resource === undefined ? {} : { resource }),
374
+ run: (context) => {
375
+ const returned: unknown = Reflect.apply(run, undefined, [context]);
376
+ return returned;
377
+ },
378
+ };
379
+ });
380
+ const definition: WorkflowDefinition<unknown, unknown> = {
381
+ kind: "hraness-kb-workflow",
382
+ version: 1,
383
+ id: value.id,
384
+ nodes,
385
+ output: value.output,
386
+ };
387
+ validateWorkflow(definition);
388
+ return definition;
389
+ }
390
+
391
+ export function checkedConcurrency(value: number | undefined): number {
392
+ const concurrency = value ?? 4;
393
+ if (
394
+ !Number.isSafeInteger(concurrency)
395
+ || concurrency < 1
396
+ || concurrency > MAX_WORKFLOW_CONCURRENCY
397
+ ) {
398
+ throw new RangeError(
399
+ `Workflow concurrency must be an integer from 1 through ${MAX_WORKFLOW_CONCURRENCY}.`,
400
+ );
401
+ }
402
+ return concurrency;
403
+ }
404
+
405
+ export function resourceLimits(
406
+ concurrency: number,
407
+ configured: WorkflowRunOptions<unknown, unknown>["resourceConcurrency"],
408
+ ): Readonly<Record<WorkflowResource, number>> {
409
+ const limits: Record<WorkflowResource, number> = {
410
+ default: concurrency,
411
+ git: Math.min(MAX_GIT_WORKFLOW_CONCURRENCY, concurrency),
412
+ qmd: 1,
413
+ };
414
+ for (const resource of ["default", "git", "qmd"] as const) {
415
+ const value = configured?.[resource];
416
+ if (value === undefined) continue;
417
+ if (resource === "qmd" && value !== 1) {
418
+ throw new RangeError("Workflow qmd concurrency is fixed at 1.");
419
+ }
420
+ if (!Number.isSafeInteger(value) || value < 1 || value > MAX_WORKFLOW_CONCURRENCY) {
421
+ throw new RangeError(
422
+ `Workflow ${resource} concurrency must be an integer from 1 through ${MAX_WORKFLOW_CONCURRENCY}.`,
423
+ );
424
+ }
425
+ limits[resource] = resource === "git"
426
+ ? Math.min(value, concurrency, MAX_GIT_WORKFLOW_CONCURRENCY)
427
+ : Math.min(value, concurrency);
428
+ }
429
+ return limits;
430
+ }
431
+
432
+ export function checkedOutputBytes(value: number | undefined): number {
433
+ const maximum = value ?? DEFAULT_WORKFLOW_OUTPUT_BYTES;
434
+ if (
435
+ !Number.isSafeInteger(maximum)
436
+ || maximum < 1
437
+ || maximum > MAX_WORKFLOW_OUTPUT_BYTES
438
+ ) {
439
+ throw new RangeError(
440
+ `Workflow output byte limit must be an integer from 1 through ${MAX_WORKFLOW_OUTPUT_BYTES}.`,
441
+ );
442
+ }
443
+ return maximum;
444
+ }
@@ -0,0 +1,74 @@
1
+ import { serialize } from "node:v8";
2
+ import { Effect } from "effect";
3
+
4
+ import { WorkflowRunError } from "./workflow-model.js";
5
+
6
+ export type NodeOutcome =
7
+ | { readonly ok: true; readonly id: string; readonly value: unknown }
8
+ | { readonly ok: false; readonly id: string; readonly error: unknown };
9
+
10
+ export type NativeCallback = { readonly completion: Promise<NodeOutcome> };
11
+
12
+ /** Register both outcomes before exposing an admitted callback to its owner. */
13
+ export function admitCallback(id: string, run: () => unknown): NativeCallback {
14
+ const completion = Promise.resolve()
15
+ .then(run)
16
+ .then((value): NodeOutcome => ({ ok: true, id, value }))
17
+ .catch((error: unknown): NodeOutcome => ({ ok: false, id, error }));
18
+ return { completion };
19
+ }
20
+
21
+ export function awaitCallback(callback: NativeCallback): Effect.Effect<NodeOutcome> {
22
+ return Effect.promise(() => callback.completion);
23
+ }
24
+
25
+ /** Native Promise ties and observation microtasks are part of the public contract. */
26
+ export function nextCallback(
27
+ callbacks: Iterable<NativeCallback>,
28
+ ): Effect.Effect<NodeOutcome> {
29
+ return Effect.promise(() => Promise.race(
30
+ Array.from(callbacks, ({ completion }) => completion),
31
+ ));
32
+ }
33
+
34
+ export function structuredOutputBytes(value: unknown, node: string) {
35
+ return Effect.try({
36
+ try: () => serialize(value).byteLength,
37
+ catch: (error: unknown) => new WorkflowRunError(
38
+ "node-failed",
39
+ `Workflow node ${JSON.stringify(node)} returned a result that cannot be structurally serialized.`,
40
+ { node, cause: error },
41
+ ),
42
+ });
43
+ }
44
+
45
+ /** Private phase marker: a native finally failure must override the body failure. */
46
+ export class WorkflowListenerRemovalFailure {
47
+ readonly reason: unknown;
48
+
49
+ constructor(reason: unknown) {
50
+ this.reason = reason;
51
+ }
52
+ }
53
+
54
+ export function workflowCancellation(signal: AbortSignal | undefined) {
55
+ return Effect.acquireRelease(
56
+ Effect.sync(() => {
57
+ const controller = new AbortController();
58
+ let abortedByCaller = false;
59
+ const abort = () => {
60
+ abortedByCaller = true;
61
+ controller.abort(signal?.reason);
62
+ };
63
+ signal?.addEventListener("abort", abort, { once: true });
64
+ return { controller, abort, get abortedByCaller() { return abortedByCaller; } };
65
+ }),
66
+ ({ abort }) => Effect.sync(() => {
67
+ try {
68
+ signal?.removeEventListener("abort", abort);
69
+ } catch (error: unknown) {
70
+ throw new WorkflowListenerRemovalFailure(error);
71
+ }
72
+ }),
73
+ );
74
+ }
@@ -0,0 +1,183 @@
1
+ import { Effect, Fiber } from "effect";
2
+ import type { Scope } from "effect";
3
+
4
+ import { needsFor, WorkflowRunError } from "./workflow-model.js";
5
+ import type {
6
+ WorkflowDefinition,
7
+ WorkflowNode,
8
+ WorkflowNodeContext,
9
+ WorkflowNodeTrace,
10
+ WorkflowResource,
11
+ WorkflowResultId,
12
+ WorkflowResultValue,
13
+ WorkflowRunOptions,
14
+ WorkflowRunResult,
15
+ } from "./workflow-model.js";
16
+ import {
17
+ admitCallback,
18
+ awaitCallback,
19
+ nextCallback,
20
+ structuredOutputBytes,
21
+ workflowCancellation,
22
+ } from "./workflow-platform.js";
23
+ import type { NativeCallback, NodeOutcome } from "./workflow-platform.js";
24
+
25
+ type NodeTask = {
26
+ readonly callback: NativeCallback;
27
+ readonly fiber: Fiber.RuntimeFiber<NodeOutcome>;
28
+ readonly resource: WorkflowResource;
29
+ };
30
+
31
+ function abortedError(): WorkflowRunError {
32
+ return new WorkflowRunError("aborted", "Workflow execution was aborted.");
33
+ }
34
+
35
+ /** One finite owner; native completion and fiber lifetime are deliberately distinct. */
36
+ export function executeWorkflow<
37
+ Input,
38
+ KnowledgeBase,
39
+ Results extends object,
40
+ Output extends WorkflowResultId<Results>,
41
+ >(
42
+ definition: WorkflowDefinition<Input, KnowledgeBase, Results, Output>,
43
+ options: WorkflowRunOptions<Input, KnowledgeBase>,
44
+ policy: {
45
+ readonly concurrency: number;
46
+ readonly limits: Readonly<Record<WorkflowResource, number>>;
47
+ readonly outputLimit: number;
48
+ },
49
+ ): Effect.Effect<WorkflowRunResult<Results, Output>, WorkflowRunError, Scope.Scope> {
50
+ return Effect.gen(function*() {
51
+ const cancellation = yield* workflowCancellation(options.signal);
52
+ const { controller } = cancellation;
53
+ const pending = new Set(definition.nodes.map(({ id }) => id));
54
+ const completed = new Set<string>();
55
+ const values = new Map<WorkflowResultId<Results>, WorkflowResultValue<Results>>();
56
+ const active = new Map<string, NodeTask>();
57
+ let failure: WorkflowRunError | undefined;
58
+ let outputBytes = 0;
59
+
60
+ const completedValue = <Id extends WorkflowResultId<Results>>(id: Id): Results[Id] => {
61
+ if (!values.has(id)) {
62
+ throw new Error(
63
+ `Workflow result ${JSON.stringify(id)} is unavailable after its dependency completed.`,
64
+ );
65
+ }
66
+ return values.get(id) as Results[Id];
67
+ };
68
+ const contextFor = (node: WorkflowNode<Input, KnowledgeBase, Results>) => {
69
+ const needs = needsFor(node);
70
+ const allowedResults = new Set(needs);
71
+ const dependencyResults = new Map<WorkflowResultId<Results>, WorkflowResultValue<Results>>(
72
+ needs.map((id) => [id, completedValue(id)] as const),
73
+ );
74
+ const context: WorkflowNodeContext<Input, KnowledgeBase, Results> = {
75
+ input: options.input,
76
+ kb: options.kb,
77
+ signal: controller.signal,
78
+ results: dependencyResults,
79
+ result: <Id extends WorkflowResultId<Results>>(id: Id): Results[Id] => {
80
+ if (!allowedResults.has(id)) {
81
+ throw new Error(
82
+ `Workflow result ${JSON.stringify(id)} is not a declared dependency of ${JSON.stringify(node.id)}.`,
83
+ );
84
+ }
85
+ return dependencyResults.get(id) as Results[Id];
86
+ },
87
+ };
88
+ return context;
89
+ };
90
+
91
+ while (completed.size < definition.nodes.length && failure === undefined) {
92
+ if (controller.signal.aborted) {
93
+ failure = abortedError();
94
+ break;
95
+ }
96
+ for (const node of definition.nodes) {
97
+ if (!pending.has(node.id) || active.size >= policy.concurrency) continue;
98
+ if (!needsFor(node).every((dependency) => completed.has(dependency))) continue;
99
+ const resource = node.resource ?? "default";
100
+ const occupied = Array.from(active.values()).filter((task) => task.resource === resource).length;
101
+ if (occupied >= policy.limits[resource]) continue;
102
+ const context = contextFor(node);
103
+ // Admission and the scope registration are one masked handoff. Scope
104
+ // closure joins the real callback, even when it ignores its signal.
105
+ const task = yield* Effect.uninterruptible(Effect.gen(function*() {
106
+ const callback = yield* Effect.sync(() => admitCallback(node.id, () => node.run(context)));
107
+ const fiber = yield* Effect.forkScoped(Effect.uninterruptible(awaitCallback(callback)));
108
+ return { callback, fiber, resource };
109
+ }));
110
+ pending.delete(node.id);
111
+ active.set(node.id, task);
112
+ }
113
+ if (active.size === 0) {
114
+ return yield* Effect.die(new Error("Workflow scheduler made no progress after validation."));
115
+ }
116
+
117
+ const outcome = yield* nextCallback(Array.from(active.values(), ({ callback }) => callback));
118
+ const task = active.get(outcome.id);
119
+ if (task !== undefined) yield* Fiber.await(task.fiber).pipe(Effect.asVoid);
120
+ active.delete(outcome.id);
121
+ if (outcome.ok) {
122
+ const measured = yield* Effect.either(structuredOutputBytes(outcome.value, outcome.id));
123
+ if (measured._tag === "Left") {
124
+ failure = measured.left;
125
+ } else if (measured.right > policy.outputLimit - outputBytes) {
126
+ failure = new WorkflowRunError(
127
+ "output-limit",
128
+ `Workflow results exceed the ${policy.outputLimit}-byte output limit at node ${JSON.stringify(outcome.id)}.`,
129
+ { node: outcome.id },
130
+ );
131
+ } else {
132
+ outputBytes += measured.right;
133
+ values.set(outcome.id as WorkflowResultId<Results>, outcome.value as WorkflowResultValue<Results>);
134
+ completed.add(outcome.id);
135
+ }
136
+ if (failure !== undefined) controller.abort(failure);
137
+ } else {
138
+ controller.abort(outcome.error);
139
+ failure = cancellation.abortedByCaller
140
+ ? abortedError()
141
+ : new WorkflowRunError(
142
+ "node-failed",
143
+ `Workflow node ${JSON.stringify(outcome.id)} failed.`,
144
+ { node: outcome.id, cause: outcome.error },
145
+ );
146
+ }
147
+ }
148
+
149
+ // Do not race interruption against callback completion. Joining the scoped
150
+ // children also observes every rejection after the selected domain failure.
151
+ yield* Effect.forEach(active.values(), ({ fiber }) => Fiber.await(fiber).pipe(Effect.asVoid), { discard: true });
152
+ if (failure !== undefined) return yield* Effect.fail(failure);
153
+
154
+ let finalOutputBytes = 0;
155
+ for (const node of definition.nodes) {
156
+ const resultBytes = yield* structuredOutputBytes(values.get(node.id), node.id);
157
+ if (resultBytes > policy.outputLimit - finalOutputBytes) {
158
+ return yield* Effect.fail(new WorkflowRunError(
159
+ "output-limit",
160
+ `Workflow results exceed the ${policy.outputLimit}-byte output limit at node ${JSON.stringify(node.id)}.`,
161
+ { node: node.id },
162
+ ));
163
+ }
164
+ finalOutputBytes += resultBytes;
165
+ }
166
+ const orderedValues = new Map<WorkflowResultId<Results>, WorkflowResultValue<Results>>(
167
+ definition.nodes.map((node) => [node.id, completedValue(node.id)] as const),
168
+ );
169
+ return {
170
+ workflow: definition.id,
171
+ outputNode: definition.output,
172
+ output: values.get(definition.output) as Results[Output],
173
+ outputBytes: finalOutputBytes,
174
+ trace: definition.nodes.map((node): WorkflowNodeTrace => ({
175
+ id: node.id,
176
+ needs: needsFor(node),
177
+ resource: node.resource ?? "default",
178
+ status: "completed",
179
+ })),
180
+ results: orderedValues,
181
+ };
182
+ });
183
+ }