@grida/fx 0.0.0-stage → 0.1.0-alpha.2
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.
- package/README.md +93 -2
- package/bin/grida-fx.js +5 -0
- package/dist/api.d.ts +128 -0
- package/dist/api.js +319 -0
- package/dist/binary.d.ts +49 -0
- package/dist/binary.js +119 -0
- package/dist/cli.d.ts +25 -0
- package/dist/cli.js +114 -0
- package/dist/documents.d.ts +213 -0
- package/dist/documents.js +7 -0
- package/dist/engine.d.ts +43 -0
- package/dist/engine.js +130 -0
- package/dist/errors.d.ts +33 -0
- package/dist/errors.js +27 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +12 -0
- package/dist/platforms.d.ts +31 -0
- package/dist/platforms.js +61 -0
- package/dist/request.d.ts +62 -0
- package/dist/request.js +142 -0
- package/dist/workflow.d.ts +190 -0
- package/dist/workflow.js +33 -0
- package/dist/yaml.d.ts +21 -0
- package/dist/yaml.js +288 -0
- package/package.json +50 -4
package/dist/errors.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Errors of `@grida/fx`. */
|
|
2
|
+
/**
|
|
3
|
+
* The engine stopped with an error (exit status 2: unreadable input or a usage mistake), would
|
|
4
|
+
* not run (`refused: …`), was interrupted (130), or could not be found or started. `message` is
|
|
5
|
+
* the engine's own message, without its `grida-fx: ` prefix.
|
|
6
|
+
*/
|
|
7
|
+
export class FxError extends Error {
|
|
8
|
+
/** The engine's exit status; `null` when it never started or ended by a signal. */
|
|
9
|
+
exitCode;
|
|
10
|
+
/** The signal that ended the engine, if one did. */
|
|
11
|
+
signal;
|
|
12
|
+
/** What the engine wrote on stderr. */
|
|
13
|
+
stderr;
|
|
14
|
+
/** What the engine wrote on stdout. */
|
|
15
|
+
stdout;
|
|
16
|
+
/** The arguments the engine was given (after the binary). */
|
|
17
|
+
args;
|
|
18
|
+
constructor(message, details = {}) {
|
|
19
|
+
super(message, details.cause === undefined ? undefined : { cause: details.cause });
|
|
20
|
+
this.name = "FxError";
|
|
21
|
+
this.exitCode = details.exitCode ?? null;
|
|
22
|
+
this.signal = details.signal ?? null;
|
|
23
|
+
this.stderr = details.stderr ?? "";
|
|
24
|
+
this.stdout = details.stdout ?? "";
|
|
25
|
+
this.args = details.args ?? [];
|
|
26
|
+
}
|
|
27
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grida FX for JavaScript. The engine is the `grida-fx` command (this package installs it, with
|
|
3
|
+
* the engine package for the machine); this package drives it: plan, price and run workflows,
|
|
4
|
+
* read their runs, and write workflow documents.
|
|
5
|
+
*/
|
|
6
|
+
export declare const version = "0.1.0-alpha.2";
|
|
7
|
+
export { engineVersion, expand, identity, type InspectOptions, inspect, Plan, PlanRefused, plan, price, project, RunFile, RunResult, type RunValue, run, } from "./api.js";
|
|
8
|
+
export { BINARY_VARIABLE, type BinaryOptions, binary, type Environment } from "./binary.js";
|
|
9
|
+
export type { Encoded, FileRef, Graph, GraphInstance, Identities, InspectedStep, Inspection, Price, PricePhase, PriceRange, Problem, ProjectedInstance, Projection, RunEvent, RunFinished, WithValue, } from "./documents.js";
|
|
10
|
+
export type { CallOptions } from "./engine.js";
|
|
11
|
+
export { FxError, type FxErrorDetails } from "./errors.js";
|
|
12
|
+
export { type Platform, platformFor, platforms } from "./platforms.js";
|
|
13
|
+
export type { PlanOptions, RunOptions } from "./request.js";
|
|
14
|
+
export { type Assertion, type Budget, type Expression, expr, type GroupStep, type InputDeclaration, type InputField, type InputRef, type JsonValue, type KeepBest, type NodeStep, type Regeneration, type Step, type WorkflowDocument, workflow, } from "./workflow.js";
|
|
15
|
+
export { needsQuotes, toYaml } from "./yaml.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grida FX for JavaScript. The engine is the `grida-fx` command (this package installs it, with
|
|
3
|
+
* the engine package for the machine); this package drives it: plan, price and run workflows,
|
|
4
|
+
* read their runs, and write workflow documents.
|
|
5
|
+
*/
|
|
6
|
+
export const version = "0.1.0-alpha.2";
|
|
7
|
+
export { engineVersion, expand, identity, inspect, Plan, PlanRefused, plan, price, project, RunFile, RunResult, run, } from "./api.js";
|
|
8
|
+
export { BINARY_VARIABLE, binary } from "./binary.js";
|
|
9
|
+
export { FxError } from "./errors.js";
|
|
10
|
+
export { platformFor, platforms } from "./platforms.js";
|
|
11
|
+
export { expr, workflow, } from "./workflow.js";
|
|
12
|
+
export { needsQuotes, toYaml } from "./yaml.js";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The platforms the preview ships the engine for, one optional npm package each (as esbuild
|
|
3
|
+
* does). `@grida/fx` depends on all four as optional dependencies; npm installs the one whose
|
|
4
|
+
* `os`, `cpu` (and on Linux `libc`) fields match the machine. `tools/build_npm.mjs` reads this
|
|
5
|
+
* table to make those packages, so it is the one list of them.
|
|
6
|
+
*
|
|
7
|
+
* Windows is not in the preview: the engine's runner relies on Unix process groups and signals.
|
|
8
|
+
*/
|
|
9
|
+
/** One platform the engine is built for. */
|
|
10
|
+
export interface Platform {
|
|
11
|
+
/** The Rust target triple the binary is built for. */
|
|
12
|
+
readonly triple: string;
|
|
13
|
+
/** The npm package that carries the binary, at `bin/grida-fx`. */
|
|
14
|
+
readonly package: string;
|
|
15
|
+
/** `process.platform` on that machine, and the package's `os` field. */
|
|
16
|
+
readonly os: "darwin" | "linux";
|
|
17
|
+
/** `process.arch` on that machine, and the package's `cpu` field. */
|
|
18
|
+
readonly cpu: "arm64" | "x64";
|
|
19
|
+
/** The package's `libc` field, for Linux: the binary links glibc (2.28 or later). */
|
|
20
|
+
readonly libc: "glibc" | null;
|
|
21
|
+
/** For people: what the platform is. */
|
|
22
|
+
readonly label: string;
|
|
23
|
+
}
|
|
24
|
+
/** Every platform of the preview. */
|
|
25
|
+
export declare const platforms: readonly Platform[];
|
|
26
|
+
/** The platform for `process.platform` and `process.arch`, or `null` when the preview has none. */
|
|
27
|
+
export declare function platformFor(os: string, cpu: string): Platform | null;
|
|
28
|
+
/** The platform built for a Rust target triple, or `null`. */
|
|
29
|
+
export declare function platformForTriple(triple: string): Platform | null;
|
|
30
|
+
/** Why the preview has no engine for this machine, in a sentence. */
|
|
31
|
+
export declare function unsupportedReason(os: string, cpu: string): string;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The platforms the preview ships the engine for, one optional npm package each (as esbuild
|
|
3
|
+
* does). `@grida/fx` depends on all four as optional dependencies; npm installs the one whose
|
|
4
|
+
* `os`, `cpu` (and on Linux `libc`) fields match the machine. `tools/build_npm.mjs` reads this
|
|
5
|
+
* table to make those packages, so it is the one list of them.
|
|
6
|
+
*
|
|
7
|
+
* Windows is not in the preview: the engine's runner relies on Unix process groups and signals.
|
|
8
|
+
*/
|
|
9
|
+
/** Every platform of the preview. */
|
|
10
|
+
export const platforms = [
|
|
11
|
+
{
|
|
12
|
+
triple: "aarch64-apple-darwin",
|
|
13
|
+
package: "@grida/fx-darwin-arm64",
|
|
14
|
+
os: "darwin",
|
|
15
|
+
cpu: "arm64",
|
|
16
|
+
libc: null,
|
|
17
|
+
label: "macOS on Apple silicon (arm64)",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
triple: "x86_64-apple-darwin",
|
|
21
|
+
package: "@grida/fx-darwin-x64",
|
|
22
|
+
os: "darwin",
|
|
23
|
+
cpu: "x64",
|
|
24
|
+
libc: null,
|
|
25
|
+
label: "macOS on Intel (x64)",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
triple: "x86_64-unknown-linux-gnu",
|
|
29
|
+
package: "@grida/fx-linux-x64-gnu",
|
|
30
|
+
os: "linux",
|
|
31
|
+
cpu: "x64",
|
|
32
|
+
libc: "glibc",
|
|
33
|
+
label: "Linux on x64 with glibc 2.28 or later",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
triple: "aarch64-unknown-linux-gnu",
|
|
37
|
+
package: "@grida/fx-linux-arm64-gnu",
|
|
38
|
+
os: "linux",
|
|
39
|
+
cpu: "arm64",
|
|
40
|
+
libc: "glibc",
|
|
41
|
+
label: "Linux on arm64 with glibc 2.28 or later",
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
/** The platform for `process.platform` and `process.arch`, or `null` when the preview has none. */
|
|
45
|
+
export function platformFor(os, cpu) {
|
|
46
|
+
return platforms.find((platform) => platform.os === os && platform.cpu === cpu) ?? null;
|
|
47
|
+
}
|
|
48
|
+
/** The platform built for a Rust target triple, or `null`. */
|
|
49
|
+
export function platformForTriple(triple) {
|
|
50
|
+
return platforms.find((platform) => platform.triple === triple) ?? null;
|
|
51
|
+
}
|
|
52
|
+
/** Why the preview has no engine for this machine, in a sentence. */
|
|
53
|
+
export function unsupportedReason(os, cpu) {
|
|
54
|
+
const supported = platforms.map((platform) => platform.label).join("; ");
|
|
55
|
+
const machine = `${os} on ${cpu}`;
|
|
56
|
+
if (os === "win32") {
|
|
57
|
+
return (`the preview of grida-fx does not run on Windows (the engine's runner uses Unix process ` +
|
|
58
|
+
`groups and signals). It runs on: ${supported}. On Windows, use WSL.`);
|
|
59
|
+
}
|
|
60
|
+
return `the preview of grida-fx has no engine for ${machine}. It runs on: ${supported}.`;
|
|
61
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The command line of a request: a target and its options as the engine's planning verbs and
|
|
3
|
+
* `run` take them (`docs/guide/05-running.md`). Every option goes as `--name=value`, so a value
|
|
4
|
+
* that looks like a flag stays a value.
|
|
5
|
+
*/
|
|
6
|
+
import type { Environment } from "./binary.js";
|
|
7
|
+
import type { CallOptions } from "./engine.js";
|
|
8
|
+
/** Options of the planning verbs (`plan`, `expand`, `identity`, `price`) and of `run`. */
|
|
9
|
+
export interface PlanOptions extends CallOptions {
|
|
10
|
+
/** Input values by name. They are written to a temporary inputs file in `cwd` (so relative
|
|
11
|
+
* paths in them keep their meaning) and given after `inputFiles`, so they win. */
|
|
12
|
+
readonly inputs?: Readonly<Record<string, unknown>>;
|
|
13
|
+
/** Inputs YAML files (`--inputs`), relative to `cwd`; later files win. */
|
|
14
|
+
readonly inputFiles?: readonly string[];
|
|
15
|
+
/** Route tables (`--routes`, `fx: routes/v1`); giving any leaves the built-in table out. */
|
|
16
|
+
readonly routes?: readonly string[];
|
|
17
|
+
/** Arguments of a builder target `file.py:function` (`--arg NAME=VALUE`). */
|
|
18
|
+
readonly args?: Readonly<Record<string, string | number | boolean>>;
|
|
19
|
+
/** The ceiling in US dollars (`--max-usd`); a live run needs one (here, in the workflow or in
|
|
20
|
+
* fx.yaml). */
|
|
21
|
+
readonly maxUsd?: number | string;
|
|
22
|
+
}
|
|
23
|
+
/** Options of `run`: the planning options, plus the run's own. */
|
|
24
|
+
export interface RunOptions extends PlanOptions {
|
|
25
|
+
/** Admit paid provider calls (`--live`). Only `true` does; nothing turns it on by default. */
|
|
26
|
+
readonly live?: boolean;
|
|
27
|
+
/** Stop before a later phase that may take the run past this amount (`--yes-up-to`). */
|
|
28
|
+
readonly yesUpTo?: number | string;
|
|
29
|
+
/** The run folder, relative to `cwd` (`--run`); a folder holding a run of the same plan is
|
|
30
|
+
* resumed. Default: a new folder under the project's runs folder. */
|
|
31
|
+
readonly runDir?: string;
|
|
32
|
+
/** Copy outputs out after the run (`--deliver OUTPUT=PATH`): `{ output: path }`, with `{key}`
|
|
33
|
+
* in the path for each element of a collection. */
|
|
34
|
+
readonly deliver?: Readonly<Record<string, string>>;
|
|
35
|
+
}
|
|
36
|
+
/** A target with its planning options, resolved. */
|
|
37
|
+
export interface PlanRequest {
|
|
38
|
+
readonly target: string;
|
|
39
|
+
readonly inputs: Readonly<Record<string, unknown>> | null;
|
|
40
|
+
readonly inputFiles: readonly string[];
|
|
41
|
+
readonly routes: readonly string[];
|
|
42
|
+
readonly args: readonly (readonly [string, string])[];
|
|
43
|
+
readonly maxUsd: string | null;
|
|
44
|
+
/** Absolute. */
|
|
45
|
+
readonly cwd: string;
|
|
46
|
+
readonly env: Environment | undefined;
|
|
47
|
+
}
|
|
48
|
+
/** The request of `target` and `options`. */
|
|
49
|
+
export declare function makeRequest(target: string, options: PlanOptions): PlanRequest;
|
|
50
|
+
/** The target and the planning options; `inputsFile` holds `request.inputs`. */
|
|
51
|
+
export declare function planningArgs(request: PlanRequest, inputsFile: string | null): string[];
|
|
52
|
+
/** The options of `run` alone. */
|
|
53
|
+
export declare function runArgs(options: RunOptions): string[];
|
|
54
|
+
/**
|
|
55
|
+
* An amount of US dollars as the command line takes it: decimal digits, never an exponent. A
|
|
56
|
+
* string is passed as it is; the engine reads it with its money rules and refuses what they do
|
|
57
|
+
* not allow.
|
|
58
|
+
*/
|
|
59
|
+
export declare function amountText(option: string, value: number | string | bigint): string;
|
|
60
|
+
/** Runs `work` with `request.inputs` in a temporary inputs file in `cwd` (its name, relative);
|
|
61
|
+
* the file is removed after. */
|
|
62
|
+
export declare function withInputsFile<T>(request: PlanRequest, work: (inputsFile: string | null) => Promise<T>): Promise<T>;
|
package/dist/request.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The command line of a request: a target and its options as the engine's planning verbs and
|
|
3
|
+
* `run` take them (`docs/guide/05-running.md`). Every option goes as `--name=value`, so a value
|
|
4
|
+
* that looks like a flag stays a value.
|
|
5
|
+
*/
|
|
6
|
+
import { randomBytes } from "node:crypto";
|
|
7
|
+
import { unlink, writeFile } from "node:fs/promises";
|
|
8
|
+
import { join, resolve } from "node:path";
|
|
9
|
+
import { toYaml } from "./yaml.js";
|
|
10
|
+
/** The request of `target` and `options`. */
|
|
11
|
+
export function makeRequest(target, options) {
|
|
12
|
+
if (typeof target !== "string" || target === "") {
|
|
13
|
+
throw new TypeError("a target is a workflow file, a workflow id or a builder file.py:function, as a string");
|
|
14
|
+
}
|
|
15
|
+
if (target.startsWith("-")) {
|
|
16
|
+
throw new TypeError(`a target cannot start with "-" (${target}): write it as ./${target}`);
|
|
17
|
+
}
|
|
18
|
+
const inputs = options.inputs ?? null;
|
|
19
|
+
if (inputs !== null && (typeof inputs !== "object" || Array.isArray(inputs))) {
|
|
20
|
+
throw new TypeError("inputs is an object of input names to values");
|
|
21
|
+
}
|
|
22
|
+
const args = Object.entries(options.args ?? {}).map(([name, value]) => {
|
|
23
|
+
if (!["string", "number", "boolean"].includes(typeof value)) {
|
|
24
|
+
throw new TypeError(`args.${name} is a string, a number or a boolean`);
|
|
25
|
+
}
|
|
26
|
+
return [name, String(value)];
|
|
27
|
+
});
|
|
28
|
+
return {
|
|
29
|
+
target,
|
|
30
|
+
inputs: inputs !== null && Object.keys(inputs).length > 0 ? inputs : null,
|
|
31
|
+
inputFiles: strings("inputFiles", options.inputFiles),
|
|
32
|
+
routes: strings("routes", options.routes),
|
|
33
|
+
args,
|
|
34
|
+
maxUsd: options.maxUsd === undefined ? null : amountText("maxUsd", options.maxUsd),
|
|
35
|
+
cwd: resolve(options.cwd ?? process.cwd()),
|
|
36
|
+
env: options.env,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/** The target and the planning options; `inputsFile` holds `request.inputs`. */
|
|
40
|
+
export function planningArgs(request, inputsFile) {
|
|
41
|
+
const args = [request.target];
|
|
42
|
+
for (const path of request.inputFiles) {
|
|
43
|
+
args.push(`--inputs=${path}`);
|
|
44
|
+
}
|
|
45
|
+
if (inputsFile !== null) {
|
|
46
|
+
args.push(`--inputs=${inputsFile}`);
|
|
47
|
+
}
|
|
48
|
+
for (const path of request.routes) {
|
|
49
|
+
args.push(`--routes=${path}`);
|
|
50
|
+
}
|
|
51
|
+
for (const [name, value] of request.args) {
|
|
52
|
+
args.push(`--arg=${name}=${value}`);
|
|
53
|
+
}
|
|
54
|
+
if (request.maxUsd !== null) {
|
|
55
|
+
args.push(`--max-usd=${request.maxUsd}`);
|
|
56
|
+
}
|
|
57
|
+
return args;
|
|
58
|
+
}
|
|
59
|
+
/** The options of `run` alone. */
|
|
60
|
+
export function runArgs(options) {
|
|
61
|
+
const args = [];
|
|
62
|
+
if (options.live !== undefined && typeof options.live !== "boolean") {
|
|
63
|
+
throw new TypeError(`live is true or false, not ${JSON.stringify(options.live)}`);
|
|
64
|
+
}
|
|
65
|
+
if (options.live === true) {
|
|
66
|
+
args.push("--live");
|
|
67
|
+
}
|
|
68
|
+
if (options.yesUpTo !== undefined) {
|
|
69
|
+
args.push(`--yes-up-to=${amountText("yesUpTo", options.yesUpTo)}`);
|
|
70
|
+
}
|
|
71
|
+
if (options.runDir !== undefined) {
|
|
72
|
+
if (typeof options.runDir !== "string" || options.runDir === "") {
|
|
73
|
+
throw new TypeError("runDir is a folder path");
|
|
74
|
+
}
|
|
75
|
+
args.push(`--run=${options.runDir}`);
|
|
76
|
+
}
|
|
77
|
+
for (const [name, path] of Object.entries(options.deliver ?? {})) {
|
|
78
|
+
if (typeof path !== "string" || name === "" || path === "") {
|
|
79
|
+
throw new TypeError(`deliver.${name} is a path`);
|
|
80
|
+
}
|
|
81
|
+
args.push(`--deliver=${name}=${path}`);
|
|
82
|
+
}
|
|
83
|
+
return args;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* An amount of US dollars as the command line takes it: decimal digits, never an exponent. A
|
|
87
|
+
* string is passed as it is; the engine reads it with its money rules and refuses what they do
|
|
88
|
+
* not allow.
|
|
89
|
+
*/
|
|
90
|
+
export function amountText(option, value) {
|
|
91
|
+
if (typeof value === "string" || typeof value === "bigint") {
|
|
92
|
+
return String(value);
|
|
93
|
+
}
|
|
94
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
95
|
+
throw new TypeError(`${option} is an amount of US dollars, not ${String(value)}`);
|
|
96
|
+
}
|
|
97
|
+
const text = Object.is(value, -0) ? "0" : String(value);
|
|
98
|
+
const exponent = /^(-?)([0-9]+)(?:\.([0-9]+))?e([-+][0-9]+)$/.exec(text);
|
|
99
|
+
if (exponent === null) {
|
|
100
|
+
return text;
|
|
101
|
+
}
|
|
102
|
+
const [, sign = "", whole = "", fraction = "", power = "0"] = exponent;
|
|
103
|
+
const digits = whole + fraction;
|
|
104
|
+
const point = whole.length + Number(power);
|
|
105
|
+
let written;
|
|
106
|
+
if (point <= 0) {
|
|
107
|
+
written = `0.${"0".repeat(-point)}${digits}`;
|
|
108
|
+
}
|
|
109
|
+
else if (point >= digits.length) {
|
|
110
|
+
written = digits + "0".repeat(point - digits.length);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
written = `${digits.slice(0, point)}.${digits.slice(point)}`;
|
|
114
|
+
}
|
|
115
|
+
return sign + written.replace(/^0+(?=[0-9])/, "");
|
|
116
|
+
}
|
|
117
|
+
/** Runs `work` with `request.inputs` in a temporary inputs file in `cwd` (its name, relative);
|
|
118
|
+
* the file is removed after. */
|
|
119
|
+
export async function withInputsFile(request, work) {
|
|
120
|
+
if (request.inputs === null) {
|
|
121
|
+
return work(null);
|
|
122
|
+
}
|
|
123
|
+
const text = toYaml(request.inputs);
|
|
124
|
+
const name = `.grida-fx-inputs-${randomBytes(8).toString("hex")}.yaml`;
|
|
125
|
+
const path = join(request.cwd, name);
|
|
126
|
+
await writeFile(path, text, { encoding: "utf8", flag: "wx" });
|
|
127
|
+
try {
|
|
128
|
+
return await work(name);
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
await unlink(path).catch(() => undefined);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function strings(option, values) {
|
|
135
|
+
const list = [...(values ?? [])];
|
|
136
|
+
for (const value of list) {
|
|
137
|
+
if (typeof value !== "string" || value === "") {
|
|
138
|
+
throw new TypeError(`${option} holds paths, as strings`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return list;
|
|
142
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow documents (`fx: workflow/v1`, `spec/schemas/fx-workflow-v1.schema.json`) as
|
|
3
|
+
* TypeScript types, and a small builder. The engine gives the document its meaning and checks
|
|
4
|
+
* it when it plans; these types only help write one. Write it out with `toYaml` and plan it:
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { writeFile } from "node:fs/promises";
|
|
8
|
+
* import { expr, plan, toYaml, workflow } from "@grida/fx";
|
|
9
|
+
*
|
|
10
|
+
* const icon = workflow({
|
|
11
|
+
* id: "icon",
|
|
12
|
+
* title: "One item icon",
|
|
13
|
+
* inputs: { name: { type: "string", description: "What the item is" } },
|
|
14
|
+
* steps: {
|
|
15
|
+
* draw: {
|
|
16
|
+
* uses: "fx/image.generate@1",
|
|
17
|
+
* with: { prompt: `A single ${expr("inputs.name")} game icon`, background: "transparent" },
|
|
18
|
+
* },
|
|
19
|
+
* },
|
|
20
|
+
* outputs: { icon: expr("steps.draw.outputs.image") },
|
|
21
|
+
* });
|
|
22
|
+
* await writeFile("workflows/icon.yaml", toYaml(icon));
|
|
23
|
+
* const planned = await plan("workflows/icon.yaml", { inputs: { name: "copper lantern" } });
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
/** A JSON value as FX holds it (`spec/identity.md` §1). */
|
|
27
|
+
export type JsonValue = null | boolean | number | string | JsonValue[] | {
|
|
28
|
+
[key: string]: JsonValue;
|
|
29
|
+
};
|
|
30
|
+
/** An expression or a value that holds `${{ … }}` (kept as a string in the document). */
|
|
31
|
+
export type Expression = string;
|
|
32
|
+
/** One input declaration: a typed field, a `$ref` to another workflow's input, or a nested
|
|
33
|
+
* object of fields. */
|
|
34
|
+
export type InputDeclaration = InputField | InputRef | {
|
|
35
|
+
[field: string]: InputDeclaration;
|
|
36
|
+
};
|
|
37
|
+
/** A typed input field. */
|
|
38
|
+
export interface InputField {
|
|
39
|
+
type: "string" | "integer" | "number" | "boolean" | "file" | "files" | "list" | "map";
|
|
40
|
+
/** `file` and `files`: the file kind, or kind family, the input accepts. */
|
|
41
|
+
kind?: string;
|
|
42
|
+
/** `files`: the value is a glob pattern. */
|
|
43
|
+
glob?: boolean;
|
|
44
|
+
/** `list`: its items. */
|
|
45
|
+
items?: InputDeclaration;
|
|
46
|
+
/** `map`: its values. */
|
|
47
|
+
values?: InputDeclaration;
|
|
48
|
+
/** The input may be left out; it is then null. */
|
|
49
|
+
optional?: boolean;
|
|
50
|
+
description?: string;
|
|
51
|
+
default?: JsonValue;
|
|
52
|
+
enum?: JsonValue[];
|
|
53
|
+
minimum?: number;
|
|
54
|
+
maximum?: number;
|
|
55
|
+
exclusive_minimum?: number;
|
|
56
|
+
exclusive_maximum?: number;
|
|
57
|
+
multiple_of?: number;
|
|
58
|
+
min_length?: number;
|
|
59
|
+
max_length?: number;
|
|
60
|
+
pattern?: string;
|
|
61
|
+
min_items?: number;
|
|
62
|
+
max_items?: number;
|
|
63
|
+
unique_items?: boolean;
|
|
64
|
+
format?: string;
|
|
65
|
+
examples?: JsonValue[];
|
|
66
|
+
}
|
|
67
|
+
/** Another workflow's input: `<file>#/<json pointer>`. */
|
|
68
|
+
export interface InputRef {
|
|
69
|
+
$ref: string;
|
|
70
|
+
optional?: boolean;
|
|
71
|
+
default?: JsonValue;
|
|
72
|
+
description?: string;
|
|
73
|
+
}
|
|
74
|
+
/** A spending ceiling. */
|
|
75
|
+
export interface Budget {
|
|
76
|
+
max_usd: number;
|
|
77
|
+
}
|
|
78
|
+
/** A check made while planning, and again once what it reads exists. */
|
|
79
|
+
export interface Assertion {
|
|
80
|
+
check: Expression | boolean;
|
|
81
|
+
message: string;
|
|
82
|
+
on_fail?: "fail" | "skip";
|
|
83
|
+
}
|
|
84
|
+
/** Which take to keep: the lowest or highest of a fact. */
|
|
85
|
+
export interface KeepBest {
|
|
86
|
+
by: string;
|
|
87
|
+
order: "lowest" | "highest";
|
|
88
|
+
}
|
|
89
|
+
/** Redo a judged step, or a group, as further takes. */
|
|
90
|
+
export interface Regeneration {
|
|
91
|
+
/** Takes in all, the first included: 1 to 12, or an expression known while planning. */
|
|
92
|
+
max: number | Expression;
|
|
93
|
+
then?: "fail" | "continue" | "skip" | {
|
|
94
|
+
keep_best: KeepBest;
|
|
95
|
+
};
|
|
96
|
+
until?: Expression | null;
|
|
97
|
+
feedback?: boolean;
|
|
98
|
+
}
|
|
99
|
+
/** Fields every step may have. */
|
|
100
|
+
interface StepCommon {
|
|
101
|
+
if?: Expression | boolean | null;
|
|
102
|
+
needs?: string[];
|
|
103
|
+
/** Repeat over a list: an expression or a literal list. */
|
|
104
|
+
for_each?: Expression | JsonValue[] | null;
|
|
105
|
+
/** The name each repeated item takes (default `item`). */
|
|
106
|
+
as?: string;
|
|
107
|
+
key?: Expression | null;
|
|
108
|
+
max?: number | Expression | null;
|
|
109
|
+
matrix?: {
|
|
110
|
+
[name: string]: Expression | JsonValue[];
|
|
111
|
+
} | null;
|
|
112
|
+
assert?: Assertion[];
|
|
113
|
+
budget?: Budget | null;
|
|
114
|
+
concurrency?: number | null;
|
|
115
|
+
view?: boolean | string;
|
|
116
|
+
timeout?: number | null;
|
|
117
|
+
/** For readers; never part of what the step makes. */
|
|
118
|
+
title?: string | null;
|
|
119
|
+
description?: string | null;
|
|
120
|
+
}
|
|
121
|
+
/** A step that runs a node type. */
|
|
122
|
+
export interface NodeStep extends StepCommon {
|
|
123
|
+
/** `fx/<name>@<major>` (built-in), `./<path>#<attr>` (a project node), or `./<path>.yaml`. */
|
|
124
|
+
uses: string;
|
|
125
|
+
/** The type's inputs and params; values holding `${{ }}` are expressions. */
|
|
126
|
+
with?: {
|
|
127
|
+
[name: string]: JsonValue;
|
|
128
|
+
};
|
|
129
|
+
/** The step this judge gives a verdict on. */
|
|
130
|
+
judges?: string | null;
|
|
131
|
+
on_reject?: "fail" | "continue" | "skip" | {
|
|
132
|
+
regenerate: Regeneration;
|
|
133
|
+
};
|
|
134
|
+
takes?: number | null;
|
|
135
|
+
pick?: "manual" | "first_accepted" | {
|
|
136
|
+
best: KeepBest;
|
|
137
|
+
} | null;
|
|
138
|
+
/** `plan`: a free, local, deterministic step that runs while planning. */
|
|
139
|
+
at?: "plan" | null;
|
|
140
|
+
/** The `model@provider` serving the type's one paid capability. */
|
|
141
|
+
route?: string | null;
|
|
142
|
+
/** Features the route must support. */
|
|
143
|
+
requires?: string[];
|
|
144
|
+
independent_of?: string | string[];
|
|
145
|
+
}
|
|
146
|
+
/** A group of steps, run together, maybe regenerated as a whole. */
|
|
147
|
+
export interface GroupStep extends StepCommon {
|
|
148
|
+
steps: {
|
|
149
|
+
[name: string]: Step;
|
|
150
|
+
};
|
|
151
|
+
regenerate?: (Regeneration & {
|
|
152
|
+
until: Expression;
|
|
153
|
+
}) | null;
|
|
154
|
+
}
|
|
155
|
+
/** One step: a node type (`uses`) or a group (`steps`). */
|
|
156
|
+
export type Step = NodeStep | GroupStep;
|
|
157
|
+
/** A workflow file (`fx: workflow/v1`). */
|
|
158
|
+
export interface WorkflowDocument {
|
|
159
|
+
fx: "workflow/v1";
|
|
160
|
+
/** Lower-case words joined by `-`, at most 96 characters. */
|
|
161
|
+
id: string;
|
|
162
|
+
title: string;
|
|
163
|
+
description?: string | null;
|
|
164
|
+
inputs?: {
|
|
165
|
+
[name: string]: InputDeclaration;
|
|
166
|
+
};
|
|
167
|
+
/** Constant data, read as `tables.<name>`. */
|
|
168
|
+
tables?: {
|
|
169
|
+
[name: string]: JsonValue;
|
|
170
|
+
};
|
|
171
|
+
/** Named expressions, read as `let.<name>`. */
|
|
172
|
+
let?: {
|
|
173
|
+
[name: string]: JsonValue;
|
|
174
|
+
};
|
|
175
|
+
budget?: Budget | null;
|
|
176
|
+
assert?: Assertion[];
|
|
177
|
+
steps: {
|
|
178
|
+
[name: string]: Step;
|
|
179
|
+
};
|
|
180
|
+
/** What a run returns: expressions over steps and inputs. */
|
|
181
|
+
outputs?: {
|
|
182
|
+
[name: string]: JsonValue;
|
|
183
|
+
};
|
|
184
|
+
view?: string | null;
|
|
185
|
+
}
|
|
186
|
+
/** A workflow document: `fields` with `fx: workflow/v1`. */
|
|
187
|
+
export declare function workflow(fields: Omit<WorkflowDocument, "fx">): WorkflowDocument;
|
|
188
|
+
/** `${{ <text> }}`: an expression, to write as a value or inside a string. */
|
|
189
|
+
export declare function expr(text: string): Expression;
|
|
190
|
+
export {};
|
package/dist/workflow.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow documents (`fx: workflow/v1`, `spec/schemas/fx-workflow-v1.schema.json`) as
|
|
3
|
+
* TypeScript types, and a small builder. The engine gives the document its meaning and checks
|
|
4
|
+
* it when it plans; these types only help write one. Write it out with `toYaml` and plan it:
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { writeFile } from "node:fs/promises";
|
|
8
|
+
* import { expr, plan, toYaml, workflow } from "@grida/fx";
|
|
9
|
+
*
|
|
10
|
+
* const icon = workflow({
|
|
11
|
+
* id: "icon",
|
|
12
|
+
* title: "One item icon",
|
|
13
|
+
* inputs: { name: { type: "string", description: "What the item is" } },
|
|
14
|
+
* steps: {
|
|
15
|
+
* draw: {
|
|
16
|
+
* uses: "fx/image.generate@1",
|
|
17
|
+
* with: { prompt: `A single ${expr("inputs.name")} game icon`, background: "transparent" },
|
|
18
|
+
* },
|
|
19
|
+
* },
|
|
20
|
+
* outputs: { icon: expr("steps.draw.outputs.image") },
|
|
21
|
+
* });
|
|
22
|
+
* await writeFile("workflows/icon.yaml", toYaml(icon));
|
|
23
|
+
* const planned = await plan("workflows/icon.yaml", { inputs: { name: "copper lantern" } });
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
/** A workflow document: `fields` with `fx: workflow/v1`. */
|
|
27
|
+
export function workflow(fields) {
|
|
28
|
+
return { fx: "workflow/v1", ...fields };
|
|
29
|
+
}
|
|
30
|
+
/** `${{ <text> }}`: an expression, to write as a value or inside a string. */
|
|
31
|
+
export function expr(text) {
|
|
32
|
+
return `\${{ ${text} }}`;
|
|
33
|
+
}
|
package/dist/yaml.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writing YAML that the engine reads: the strict subset of `spec/yaml.md`, written as the engine
|
|
3
|
+
* writes it ("Writing YAML", `crates/grida-fx-core/src/yaml/write.rs`).
|
|
4
|
+
*
|
|
5
|
+
* Block style, two-space indentation, keys in the order the object holds them. A string is
|
|
6
|
+
* written plain when, read back as a plain scalar, it is that same string; otherwise it is
|
|
7
|
+
* double-quoted with JSON-style escapes. So `on`, `017`, `2026-10-05`, `1e3`, a digest like
|
|
8
|
+
* `123e4567…`, `''`, and text with `: `, ` #` or a leading indicator are all quoted. Numbers are
|
|
9
|
+
* written in their canonical form (ECMAScript's, as JCS writes them). The text ends with one
|
|
10
|
+
* line feed.
|
|
11
|
+
*
|
|
12
|
+
* Only FX values are written (`spec/identity.md` §1): `null`, booleans, finite numbers, strings
|
|
13
|
+
* without lone surrogates, arrays, and plain objects, nested at most 512 deep. An object member
|
|
14
|
+
* whose value is `undefined` is left out, as JSON leaves it out; anything else is a `TypeError`
|
|
15
|
+
* naming where it is. A key longer than 1024 characters cannot be written (YAML's limit for an
|
|
16
|
+
* implicit key).
|
|
17
|
+
*/
|
|
18
|
+
/** Writes `value` as a YAML document the engine reads back as the same value. */
|
|
19
|
+
export declare function toYaml(value: unknown): string;
|
|
20
|
+
/** Whether a string must be quoted to read back as itself. */
|
|
21
|
+
export declare function needsQuotes(text: string): boolean;
|