@fungi.computer/braid 0.0.0-stage → 0.1.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 (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +260 -2
  3. package/dist/cloudflare/graph-engine.d.ts +25 -0
  4. package/dist/cloudflare/graph-engine.js +46 -0
  5. package/dist/cloudflare/graph-program.d.ts +6 -0
  6. package/dist/cloudflare/graph-program.js +550 -0
  7. package/dist/cloudflare/index.d.ts +1 -0
  8. package/dist/cloudflare/index.js +1 -0
  9. package/dist/code-execution.d.ts +16 -0
  10. package/dist/code-execution.js +50 -0
  11. package/dist/code-result.d.ts +11 -0
  12. package/dist/code-result.js +19 -0
  13. package/dist/code-sandbox.d.ts +9 -0
  14. package/dist/code-sandbox.js +13 -0
  15. package/dist/compile.d.ts +43 -0
  16. package/dist/compile.js +248 -0
  17. package/dist/execution.d.ts +194 -0
  18. package/dist/execution.js +130 -0
  19. package/dist/expressions.d.ts +16 -0
  20. package/dist/expressions.js +322 -0
  21. package/dist/git-access.d.ts +59 -0
  22. package/dist/git-access.js +106 -0
  23. package/dist/index.d.ts +9 -0
  24. package/dist/index.js +9 -0
  25. package/dist/ports.d.ts +58 -0
  26. package/dist/ports.js +256 -0
  27. package/dist/review-text.d.ts +15 -0
  28. package/dist/review-text.js +40 -0
  29. package/dist/runtime/arming-authority.d.ts +83 -0
  30. package/dist/runtime/arming-authority.js +90 -0
  31. package/dist/runtime/bindings.d.ts +25 -0
  32. package/dist/runtime/bindings.js +41 -0
  33. package/dist/runtime/capture.d.ts +18 -0
  34. package/dist/runtime/capture.js +99 -0
  35. package/dist/runtime/git-pass-contract.d.ts +63 -0
  36. package/dist/runtime/git-pass-contract.js +36 -0
  37. package/dist/runtime/git-pass-store.d.ts +32 -0
  38. package/dist/runtime/git-pass-store.js +82 -0
  39. package/dist/runtime/git-pass.d.ts +41 -0
  40. package/dist/runtime/git-pass.js +72 -0
  41. package/dist/runtime/graph-contract.d.ts +788 -0
  42. package/dist/runtime/graph-contract.js +122 -0
  43. package/dist/runtime/graph-dispatch.d.ts +31 -0
  44. package/dist/runtime/graph-dispatch.js +111 -0
  45. package/dist/runtime/graph-run-store.d.ts +79 -0
  46. package/dist/runtime/graph-run-store.js +267 -0
  47. package/dist/runtime/graph-start.d.ts +90 -0
  48. package/dist/runtime/graph-start.js +124 -0
  49. package/dist/runtime/index.d.ts +20 -0
  50. package/dist/runtime/index.js +20 -0
  51. package/dist/runtime/ports.d.ts +156 -0
  52. package/dist/runtime/ports.js +1 -0
  53. package/dist/runtime/run-authority.d.ts +60 -0
  54. package/dist/runtime/run-authority.js +145 -0
  55. package/dist/runtime/run-ceiling.d.ts +106 -0
  56. package/dist/runtime/run-ceiling.js +23 -0
  57. package/dist/runtime/run-member.d.ts +3 -0
  58. package/dist/runtime/run-member.js +12 -0
  59. package/dist/runtime/run-owner.d.ts +1650 -0
  60. package/dist/runtime/run-owner.js +513 -0
  61. package/dist/runtime/run-report.d.ts +14 -0
  62. package/dist/runtime/run-report.js +23 -0
  63. package/dist/runtime/schedules.d.ts +39 -0
  64. package/dist/runtime/schedules.js +149 -0
  65. package/dist/runtime/stored-trigger-rows.d.ts +12 -0
  66. package/dist/runtime/stored-trigger-rows.js +17 -0
  67. package/dist/runtime/trigger-owner.d.ts +594 -0
  68. package/dist/runtime/trigger-owner.js +280 -0
  69. package/dist/runtime/trigger-plans.d.ts +774 -0
  70. package/dist/runtime/trigger-plans.js +62 -0
  71. package/dist/runtime/trigger-router.d.ts +34 -0
  72. package/dist/runtime/trigger-router.js +143 -0
  73. package/dist/runtime/trigger-run-reservations.d.ts +28 -0
  74. package/dist/runtime/trigger-run-reservations.js +114 -0
  75. package/dist/schema.d.ts +1183 -0
  76. package/dist/schema.js +239 -0
  77. package/dist/triggers.d.ts +46 -0
  78. package/dist/triggers.js +75 -0
  79. package/dist/validate.d.ts +25 -0
  80. package/dist/validate.js +686 -0
  81. package/dist/workflow-id.d.ts +3 -0
  82. package/dist/workflow-id.js +3 -0
  83. package/package.json +96 -3
@@ -0,0 +1,13 @@
1
+ import * as Effect from "effect/Effect";
2
+ import { sandboxFailure } from "@fungi.computer/mycelium/sandbox-boundary";
3
+ /**
4
+ * A Code step abandons its call when its owning Effect is interrupted. The
5
+ * sandbox receives Effect's local abort signal; trusted host budgets remain
6
+ * responsible for backing work that cannot acknowledge cancellation.
7
+ */
8
+ export function callCodeSandbox(sandbox, request) {
9
+ return Effect.tryPromise({
10
+ try: (signal) => sandbox.execute(request, signal),
11
+ catch: sandboxFailure,
12
+ });
13
+ }
@@ -0,0 +1,43 @@
1
+ import { type Issue } from "./validate.js";
2
+ import { type ExecutionPlan, type Json, type Catalog, type Bindings, type WorkflowDefinition } from "./schema.js";
3
+ /** Canonical object ordering; array order remains semantic. No execution layout is accepted. */
4
+ export declare function canonicalJson(value: Json): string;
5
+ export declare function contentDigest(value: Json): Promise<string>;
6
+ export type CompileResult = {
7
+ readonly ok: false;
8
+ readonly issues: readonly Issue[];
9
+ } | {
10
+ readonly ok: true;
11
+ readonly plan: ExecutionPlan;
12
+ };
13
+ export declare function compile(raw: WorkflowDefinition, rawCatalog: Catalog, rawBindings: Bindings): Promise<CompileResult>;
14
+ /** Stored plans are parsed and their content identity verified before interpretation. */
15
+ export declare function decodeExecutionPlan(raw: unknown): Promise<ExecutionPlan>;
16
+ /** One App method as an App's `api` block declares it; structural, no App SDK import. */
17
+ export type DeclaredMethod = Readonly<{
18
+ /** Opt in to the shared Pending control result. */
19
+ pending?: true | undefined;
20
+ /** Discovery-only metadata from the App declaration; hidden methods have no graph ports. */
21
+ hidden?: true | undefined;
22
+ /** Presentation is display metadata and never part of a method's graph contract. */
23
+ present?: object | undefined;
24
+ description: string;
25
+ input: object;
26
+ output: object;
27
+ effect: "read" | "write" | "external";
28
+ requires: readonly string[];
29
+ }>;
30
+ /**
31
+ * The catalog entry for one App's declared methods. The digest covers what a
32
+ * node depends on (ports, effect, approval, requires), not the description, so
33
+ * rewording a method does not drift Workflows. External effects queue for
34
+ * approval; v1 declares no other approval policy.
35
+ */
36
+ export declare function catalogMethods(methods: Readonly<Record<string, DeclaredMethod>>): Promise<Catalog[string]>;
37
+ /** Event identity pins the whole emitted schema; descriptions do not affect compatibility. */
38
+ export declare function catalogEvents(events: Readonly<Record<string, {
39
+ description: string;
40
+ payload: object;
41
+ }>>): Promise<{
42
+ events: NonNullable<Catalog[string]["events"]>;
43
+ }>;
@@ -0,0 +1,248 @@
1
+ import * as S from "effect/Schema";
2
+ import * as Option from "effect/Option";
3
+ import { analyze } from "./validate.js";
4
+ import { bindingsSchema, executionPlanSchema, methodSchema, eventSchema, projectionSchema, } from "./schema.js";
5
+ /** Canonical object ordering; array order remains semantic. No execution layout is accepted. */
6
+ export function canonicalJson(value) {
7
+ if (Array.isArray(value))
8
+ return "[" + value.map(canonicalJson).join(",") + "]";
9
+ if (S.is(S.JsonObject)(value))
10
+ return ("{" +
11
+ Object.keys(value)
12
+ .sort()
13
+ .map((key) => JSON.stringify(key) + ":" + canonicalJson(value[key] ?? null))
14
+ .join(",") +
15
+ "}");
16
+ return JSON.stringify(value);
17
+ }
18
+ export async function contentDigest(value) {
19
+ const json = S.decodeUnknownSync(S.Json)(value);
20
+ const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(canonicalJson(json)));
21
+ return ("sha256:" +
22
+ [...new Uint8Array(hash)]
23
+ .map((byte) => byte.toString(16).padStart(2, "0"))
24
+ .join(""));
25
+ }
26
+ export async function compile(raw, rawCatalog, rawBindings) {
27
+ const analysis = analyze(raw, rawCatalog);
28
+ if (analysis.issues.length || !analysis.definition || !analysis.catalog)
29
+ return { ok: false, issues: analysis.issues };
30
+ let bindings;
31
+ try {
32
+ bindings = S.decodeUnknownSync(bindingsSchema)(rawBindings, {
33
+ onExcessProperty: "error",
34
+ });
35
+ }
36
+ catch (cause) {
37
+ if (!S.isSchemaError(cause))
38
+ throw cause;
39
+ return {
40
+ ok: false,
41
+ issues: [{ code: "binding", message: "Invalid installation bindings" }],
42
+ };
43
+ }
44
+ const { definition, catalog } = analysis;
45
+ const requiredBindings = [];
46
+ const pinned = {};
47
+ const bound = {};
48
+ const issues = [];
49
+ const seen = new Set();
50
+ for (const node of Object.values(analysis.order).flat()) {
51
+ if (node.kind !== "app")
52
+ continue;
53
+ const app = definition.apps[node.app];
54
+ const binding = bindings[node.app];
55
+ if (!app ||
56
+ binding === undefined ||
57
+ !("installationId" in binding) ||
58
+ !binding.installationId ||
59
+ binding.appId !== app.appId) {
60
+ issues.push({
61
+ code: "binding",
62
+ node: node.id,
63
+ message: "App binding missing or targets a different App",
64
+ });
65
+ continue;
66
+ }
67
+ const method = catalog[app.appId]?.methods[node.method];
68
+ if (!method)
69
+ continue;
70
+ pinned[app.appId] = {
71
+ methods: { ...pinned[app.appId]?.methods, [node.method]: method },
72
+ };
73
+ bound[node.app] = binding;
74
+ if (node.gitAccess !== undefined) {
75
+ const target = bindings[node.gitAccess.binding];
76
+ if (target === undefined) {
77
+ issues.push({
78
+ code: "binding",
79
+ node: node.id,
80
+ message: "Git repository binding is missing",
81
+ });
82
+ continue;
83
+ }
84
+ bound[node.gitAccess.binding] = target;
85
+ }
86
+ const key = node.app + "." + node.method;
87
+ if (seen.has(key))
88
+ continue;
89
+ seen.add(key);
90
+ const { description, input, output, effect, approval, requires } = method;
91
+ requiredBindings.push({
92
+ app: node.app,
93
+ installationId: binding.installationId,
94
+ method: node.method,
95
+ description,
96
+ input,
97
+ output,
98
+ effect,
99
+ approval,
100
+ requires,
101
+ });
102
+ }
103
+ for (const trigger of definition.triggers ?? []) {
104
+ if (trigger.on !== "event")
105
+ continue;
106
+ const app = definition.apps[trigger.app];
107
+ const binding = bindings[trigger.app];
108
+ if (!app ||
109
+ binding === undefined ||
110
+ !("installationId" in binding) ||
111
+ !binding.installationId ||
112
+ binding.appId !== app.appId) {
113
+ issues.push({
114
+ code: "binding",
115
+ trigger: trigger.id,
116
+ message: "Event App binding missing or targets a different App",
117
+ });
118
+ continue;
119
+ }
120
+ const event = catalog[app.appId]?.events?.[trigger.event];
121
+ if (!event)
122
+ continue;
123
+ const prior = pinned[app.appId];
124
+ pinned[app.appId] = {
125
+ methods: prior?.methods ?? {},
126
+ events: { ...prior?.events, [trigger.event]: event },
127
+ };
128
+ bound[trigger.app] = binding;
129
+ }
130
+ if (issues.length)
131
+ return { ok: false, issues };
132
+ let sortedDefinition = {
133
+ ...definition,
134
+ nodes: analysis.order.root ?? [],
135
+ edges: [...definition.edges].sort((a, b) => canonicalJson(a).localeCompare(canonicalJson(b))),
136
+ };
137
+ if (definition.subgraphs) {
138
+ sortedDefinition = {
139
+ ...sortedDefinition,
140
+ subgraphs: Object.fromEntries(Object.entries(definition.subgraphs).map(([id, graph]) => [
141
+ id,
142
+ {
143
+ ...graph,
144
+ nodes: analysis.order[id] ?? [],
145
+ edges: [...graph.edges].sort((a, b) => canonicalJson(a).localeCompare(canonicalJson(b))),
146
+ },
147
+ ])),
148
+ };
149
+ }
150
+ const content = {
151
+ definition: sortedDefinition,
152
+ catalog: pinned,
153
+ bindings: bound,
154
+ estimatedSteps: analysis.estimatedSteps,
155
+ requiredBindings: requiredBindings.sort((a, b) => (a.app + a.method).localeCompare(b.app + b.method)),
156
+ };
157
+ return {
158
+ ok: true,
159
+ plan: { ...content, planDigest: await contentDigest(content) },
160
+ };
161
+ }
162
+ /** Stored plans are parsed and their content identity verified before interpretation. */
163
+ export async function decodeExecutionPlan(raw) {
164
+ const plan = S.decodeUnknownSync(executionPlanSchema)(raw, {
165
+ onExcessProperty: "error",
166
+ });
167
+ const { planDigest, ...content } = plan;
168
+ if ((await contentDigest(content)) !== planDigest)
169
+ throw new Error("Workflow plan digest mismatch");
170
+ const rebuilt = await compile(plan.definition, plan.catalog, plan.bindings);
171
+ if (!rebuilt.ok || rebuilt.plan.planDigest !== planDigest)
172
+ throw new Error("Workflow plan is not a compiled v1 plan");
173
+ return plan;
174
+ }
175
+ // The emitted draft identifier is metadata; graph ports use the completion schema.
176
+ function schemaView(schema) {
177
+ return Object.fromEntries(Object.entries(schema).filter(([key]) => key !== "$schema"));
178
+ }
179
+ // Agar emits exactly [declared Output, Pending]. This projects its completion
180
+ // side only; unsupported nested unions remain unsupported graph port schemas.
181
+ const emittedPendingUnion = S.Struct({
182
+ anyOf: S.Tuple([projectionSchema, projectionSchema]),
183
+ });
184
+ function completionView(method) {
185
+ const output = schemaView(method.output);
186
+ if (method.pending !== true)
187
+ return output;
188
+ const union = S.decodeUnknownOption(emittedPendingUnion, {
189
+ onExcessProperty: "error",
190
+ })(output);
191
+ return Option.isSome(union) ? schemaView(union.value.anyOf[0]) : output;
192
+ }
193
+ /** The graph-facing contract of one method: no discovery or display metadata. */
194
+ function graphContract(method) {
195
+ const { description, input, output, effect, requires } = method;
196
+ const contract = { description, input, output, effect, requires };
197
+ return method.pending === true ? { ...contract, pending: true } : contract;
198
+ }
199
+ /**
200
+ * The catalog entry for one App's declared methods. The digest covers what a
201
+ * node depends on (ports, effect, approval, requires), not the description, so
202
+ * rewording a method does not drift Workflows. External effects queue for
203
+ * approval; v1 declares no other approval policy.
204
+ */
205
+ export async function catalogMethods(methods) {
206
+ const entries = await Promise.all(Object.entries(methods)
207
+ .flatMap(([name, declared]) => declared.hidden === true
208
+ ? []
209
+ : [[name, graphContract(declared)]])
210
+ .map(async ([name, declared]) => {
211
+ const method = S.decodeUnknownSync(methodSchema)({
212
+ ...declared,
213
+ requires: [...declared.requires],
214
+ approval: declared.effect === "external" ? "queue" : "none",
215
+ }, { onExcessProperty: "error" });
216
+ const pinned = {
217
+ input: method.input,
218
+ output: method.output,
219
+ effect: method.effect,
220
+ approval: method.approval,
221
+ requires: method.requires,
222
+ };
223
+ const digestInput = method.pending === true ? { ...pinned, pending: true } : pinned;
224
+ return [
225
+ name,
226
+ {
227
+ ...method,
228
+ input: schemaView(method.input),
229
+ output: completionView(method),
230
+ digest: await contentDigest(digestInput),
231
+ },
232
+ ];
233
+ }));
234
+ return { methods: Object.fromEntries(entries) };
235
+ }
236
+ /** Event identity pins the whole emitted schema; descriptions do not affect compatibility. */
237
+ export async function catalogEvents(events) {
238
+ const entries = await Promise.all(Object.entries(events).map(async ([name, raw]) => {
239
+ const event = S.decodeUnknownSync(eventSchema)(raw, {
240
+ onExcessProperty: "error",
241
+ });
242
+ return [
243
+ name,
244
+ { ...event, digest: await contentDigest({ payload: event.payload }) },
245
+ ];
246
+ }));
247
+ return { events: Object.fromEntries(entries) };
248
+ }
@@ -0,0 +1,194 @@
1
+ import * as S from "effect/Schema";
2
+ import type { ExecutionPlan, Json, Method } from "./schema.js";
3
+ /**
4
+ * One loop a step runs inside: which Map item or Repeat round this pass is,
5
+ * counted from 0, of how many. The runtime knows both while it executes; the
6
+ * iteration id is a digest and says neither.
7
+ */
8
+ export declare const loopPlaceSchema: S.Struct<{
9
+ readonly node: S.String;
10
+ readonly loop: S.Literals<readonly ["map", "repeat"]>;
11
+ readonly index: S.Number;
12
+ readonly count: S.Number;
13
+ }>;
14
+ export type LoopPlace = typeof loopPlaceSchema.Type;
15
+ /** Outermost loop first. Nesting needs one loop node per level, so the 64-node graph bound holds. */
16
+ export declare const loopPositionSchema: S.$Array<S.Struct<{
17
+ readonly node: S.String;
18
+ readonly loop: S.Literals<readonly ["map", "repeat"]>;
19
+ readonly index: S.Number;
20
+ readonly count: S.Number;
21
+ }>>;
22
+ export type LoopPosition = typeof loopPositionSchema.Type;
23
+ export declare const graphParamsSchema: S.Union<readonly [S.Struct<{
24
+ readonly kind: S.Literal<"run">;
25
+ readonly teamId: S.String;
26
+ readonly runId: S.String;
27
+ readonly planDigest: S.String;
28
+ }>, S.Struct<{
29
+ readonly kind: S.Literal<"map-item">;
30
+ readonly teamId: S.String;
31
+ readonly runId: S.String;
32
+ readonly planDigest: S.String;
33
+ readonly body: S.String;
34
+ readonly iteration: S.String;
35
+ readonly parentInstanceId: S.String;
36
+ readonly completionEvent: S.String;
37
+ readonly input: S.$Record<S.String, S.Codec<S.Json, S.Json, never, never>>;
38
+ readonly position: S.$Array<S.Struct<{
39
+ readonly node: S.String;
40
+ readonly loop: S.Literals<readonly ["map", "repeat"]>;
41
+ readonly index: S.Number;
42
+ readonly count: S.Number;
43
+ }>>;
44
+ }>]>;
45
+ export type GraphParams = typeof graphParamsSchema.Type;
46
+ export declare const nodeEventSchema: S.Struct<{
47
+ readonly runId: S.String;
48
+ readonly graphId: S.optionalKey<S.String>;
49
+ readonly nodeId: S.String;
50
+ readonly iteration: S.String;
51
+ /** The loops around this step; absent at the top of the run. */
52
+ readonly position: S.optionalKey<S.$Array<S.Struct<{
53
+ readonly node: S.String;
54
+ readonly loop: S.Literals<readonly ["map", "repeat"]>;
55
+ readonly index: S.Number;
56
+ readonly count: S.Number;
57
+ }>>>;
58
+ readonly poll: S.optionalKey<S.Number>;
59
+ readonly state: S.Literals<readonly ["queued", "running", "waiting", "complete", "failed", "skipped"]>;
60
+ readonly attempt: S.Number;
61
+ /** Time the host durably recorded this lifecycle fact. */
62
+ readonly recordedAt: S.optionalKey<S.Number>;
63
+ readonly durationMs: S.optionalKey<S.Number>;
64
+ readonly costMilliunits: S.optionalKey<S.Number>;
65
+ readonly outputDigest: S.optionalKey<S.String>;
66
+ readonly output: S.optionalKey<S.$Record<S.String, S.Codec<S.Json, S.Json, never, never>>>;
67
+ /** How the finished App call reads to a person; absent when the App declares no presentation. */
68
+ readonly row: S.optionalKey<S.Struct<{
69
+ readonly media: S.optional<S.Struct<{
70
+ readonly path: S.String;
71
+ readonly format: S.Literals<readonly ["fungi:image", "fungi:video"]>;
72
+ }>>;
73
+ readonly id: S.String;
74
+ readonly title: S.String;
75
+ readonly detail: S.optional<S.String>;
76
+ readonly tone: S.optional<S.Literals<readonly ["attention", "running", "error", "settled", "muted"]>>;
77
+ readonly status: S.optional<S.Struct<{
78
+ readonly label: S.String;
79
+ readonly tone: S.Literals<readonly ["neutral", "primary", "secondary", "accent", "success", "info", "warning", "error"]>;
80
+ }>>;
81
+ readonly open: S.optional<S.Struct<{
82
+ readonly params: S.$Array<S.Struct<{
83
+ readonly key: S.String;
84
+ readonly value: S.String;
85
+ }>>;
86
+ }>>;
87
+ }>>;
88
+ readonly errorMessage: S.optionalKey<S.String>;
89
+ readonly refusal: S.optionalKey<S.String>;
90
+ }>;
91
+ export type NodeEvent = typeof nodeEventSchema.Type;
92
+ export declare const graphReviewSchema: S.Struct<{
93
+ readonly runId: S.String;
94
+ readonly nodeId: S.String;
95
+ readonly iteration: S.String;
96
+ /** The loops around this review; absent at the top of the run. */
97
+ readonly position: S.optionalKey<S.$Array<S.Struct<{
98
+ readonly node: S.String;
99
+ readonly loop: S.Literals<readonly ["map", "repeat"]>;
100
+ readonly index: S.Number;
101
+ readonly count: S.Number;
102
+ }>>>;
103
+ readonly instanceId: S.String;
104
+ readonly eventType: S.String;
105
+ readonly title: S.String;
106
+ readonly reviewers: S.Literals<readonly ["runner", "owners"]>;
107
+ readonly timeout: S.Number;
108
+ readonly show: S.$Array<S.Codec<S.Json, S.Json, never, never>>;
109
+ /** The review's named choices; absent for an approve/reject review. */
110
+ readonly choices: S.optionalKey<S.$Array<S.String>>;
111
+ }>;
112
+ export type GraphReview = typeof graphReviewSchema.Type;
113
+ /** Approve or reject a plain review, or pick one of a review's named choices. */
114
+ export declare const reviewAnswerSchema: S.Union<readonly [S.Struct<{
115
+ readonly approved: S.Boolean;
116
+ }>, S.Struct<{
117
+ readonly choice: S.String;
118
+ readonly note: S.optionalKey<S.String>;
119
+ }>]>;
120
+ /** Project one decoded review answer into the review node's output ports. */
121
+ export declare function reviewAnswerOutput(answer: typeof reviewAnswerSchema.Type, item: Json): {
122
+ [x: string]: S.Json;
123
+ choice: string;
124
+ note: string;
125
+ };
126
+ export declare const dispatchResultSchema: S.Union<readonly [S.Struct<{
127
+ readonly kind: S.Literal<"complete">;
128
+ readonly output: S.Codec<S.Json, S.Json, never, never>;
129
+ readonly costMilliunits: S.Number;
130
+ readonly row: S.optionalKey<S.Struct<{
131
+ readonly media: S.optional<S.Struct<{
132
+ readonly path: S.String;
133
+ readonly format: S.Literals<readonly ["fungi:image", "fungi:video"]>;
134
+ }>>;
135
+ readonly id: S.String;
136
+ readonly title: S.String;
137
+ readonly detail: S.optional<S.String>;
138
+ readonly tone: S.optional<S.Literals<readonly ["attention", "running", "error", "settled", "muted"]>>;
139
+ readonly status: S.optional<S.Struct<{
140
+ readonly label: S.String;
141
+ readonly tone: S.Literals<readonly ["neutral", "primary", "secondary", "accent", "success", "info", "warning", "error"]>;
142
+ }>>;
143
+ readonly open: S.optional<S.Struct<{
144
+ readonly params: S.$Array<S.Struct<{
145
+ readonly key: S.String;
146
+ readonly value: S.String;
147
+ }>>;
148
+ }>>;
149
+ }>>;
150
+ }>, S.Struct<{
151
+ readonly kind: S.Literal<"refused">;
152
+ readonly code: S.Literals<readonly ["not_granted", "invalid_input", "invalid_output", "revoked", "member_revoked", "workflow_run_revoked", "computer_revoked", "budget_exhausted", "unknown_method"]>;
153
+ }>, S.Struct<{
154
+ readonly kind: S.Literal<"pending_approval">;
155
+ readonly actionId: S.String;
156
+ }>, S.Struct<{
157
+ readonly kind: S.Literal<"pending">;
158
+ readonly retryAfterMs: S.Number;
159
+ }>]>;
160
+ export type DispatchResult = typeof dispatchResultSchema.Type;
161
+ /** Live admission before any node kind; refusals use the engine's sole vocabulary. */
162
+ export declare const nodeAdmissionResultSchema: S.Union<readonly [S.Struct<{
163
+ readonly kind: S.Literal<"admitted">;
164
+ }>, S.Struct<{
165
+ readonly kind: S.Literal<"refused">;
166
+ readonly code: S.Literals<readonly ["not_granted", "invalid_input", "invalid_output", "revoked", "member_revoked", "workflow_run_revoked", "computer_revoked", "budget_exhausted", "unknown_method"]>;
167
+ }>]>;
168
+ export type NodeAdmissionResult = typeof nodeAdmissionResultSchema.Type;
169
+ export type NodeCall = {
170
+ readonly teamId: string;
171
+ readonly runId: string;
172
+ readonly graphId?: string;
173
+ readonly nodeId: string;
174
+ readonly iteration: string;
175
+ readonly poll?: number;
176
+ readonly installationId: string;
177
+ readonly method: string;
178
+ readonly methodSpec: Method;
179
+ readonly input: Json;
180
+ readonly idempotencyKey: string;
181
+ };
182
+ /** Host ports: load an admitted plan, invoke Apps, and persist execution facts. */
183
+ export type NodeDispatcher = {
184
+ loadRun(teamId: string, runId: string): Promise<{
185
+ readonly plan: ExecutionPlan;
186
+ readonly input: Readonly<Record<string, Json>>;
187
+ }>;
188
+ /** Recheck the run's member and live library grant before the node executes. */
189
+ beforeNode(event: NodeEvent): Promise<NodeAdmissionResult>;
190
+ dispatch(call: NodeCall): Promise<DispatchResult>;
191
+ recordEvent(event: NodeEvent): Promise<void>;
192
+ /** Persist once and return the owner-held expiration, unchanged on replay. */
193
+ openReview(review: GraphReview): Promise<number>;
194
+ };
@@ -0,0 +1,130 @@
1
+ import * as S from "effect/Schema";
2
+ import { resultRowSchemaV1 } from "@fungi.computer/gills";
3
+ const id = S.String.pipe(S.check(S.isMinLength(1)), S.check(S.isMaxLength(256)));
4
+ const natural = S.Number.pipe(S.check(S.isInt()), S.check(S.isGreaterThanOrEqualTo(0)));
5
+ /**
6
+ * One loop a step runs inside: which Map item or Repeat round this pass is,
7
+ * counted from 0, of how many. The runtime knows both while it executes; the
8
+ * iteration id is a digest and says neither.
9
+ */
10
+ export const loopPlaceSchema = S.Struct({
11
+ node: id,
12
+ loop: S.Literals(["map", "repeat"]),
13
+ index: natural,
14
+ count: natural,
15
+ });
16
+ /** Outermost loop first. Nesting needs one loop node per level, so the 64-node graph bound holds. */
17
+ export const loopPositionSchema = S.Array(loopPlaceSchema).check(S.isMaxLength(64));
18
+ export const graphParamsSchema = S.Union([
19
+ S.Struct({ kind: S.Literal("run"), teamId: id, runId: id, planDigest: id }),
20
+ S.Struct({
21
+ kind: S.Literal("map-item"),
22
+ teamId: id,
23
+ runId: id,
24
+ planDigest: id,
25
+ body: id,
26
+ iteration: id,
27
+ parentInstanceId: id,
28
+ completionEvent: id,
29
+ input: S.JsonObject,
30
+ position: loopPositionSchema,
31
+ }),
32
+ ]);
33
+ export const nodeEventSchema = S.Struct({
34
+ runId: id,
35
+ graphId: S.optionalKey(id),
36
+ nodeId: id,
37
+ iteration: id,
38
+ /** The loops around this step; absent at the top of the run. */
39
+ position: S.optionalKey(loopPositionSchema),
40
+ poll: S.optionalKey(natural),
41
+ state: S.Literals([
42
+ "queued",
43
+ "running",
44
+ "waiting",
45
+ "complete",
46
+ "failed",
47
+ "skipped",
48
+ ]),
49
+ attempt: natural,
50
+ /** Time the host durably recorded this lifecycle fact. */
51
+ recordedAt: S.optionalKey(natural),
52
+ durationMs: S.optionalKey(natural),
53
+ costMilliunits: S.optionalKey(natural),
54
+ outputDigest: S.optionalKey(id),
55
+ output: S.optionalKey(S.JsonObject),
56
+ /** How the finished App call reads to a person; absent when the App declares no presentation. */
57
+ row: S.optionalKey(resultRowSchemaV1),
58
+ errorMessage: S.optionalKey(S.String.check(S.isMaxLength(1024))),
59
+ refusal: S.optionalKey(id),
60
+ });
61
+ export const graphReviewSchema = S.Struct({
62
+ runId: id,
63
+ nodeId: id,
64
+ iteration: id,
65
+ /** The loops around this review; absent at the top of the run. */
66
+ position: S.optionalKey(loopPositionSchema),
67
+ instanceId: id,
68
+ eventType: id,
69
+ title: S.String,
70
+ reviewers: S.Literals(["runner", "owners"]),
71
+ timeout: natural,
72
+ show: S.Array(S.Json),
73
+ /** The review's named choices; absent for an approve/reject review. */
74
+ choices: S.optionalKey(S.Array(id)),
75
+ });
76
+ /** Approve or reject a plain review, or pick one of a review's named choices. */
77
+ export const reviewAnswerSchema = S.Union([
78
+ S.Struct({ approved: S.Boolean }),
79
+ S.Struct({
80
+ choice: id,
81
+ note: S.optionalKey(S.String.check(S.isMaxLength(1000))),
82
+ }),
83
+ ]);
84
+ /** Project one decoded review answer into the review node's output ports. */
85
+ export function reviewAnswerOutput(answer, item) {
86
+ const choice = "choice" in answer
87
+ ? answer.choice
88
+ : answer.approved
89
+ ? "approved"
90
+ : "rejected";
91
+ return {
92
+ [choice]: item,
93
+ choice,
94
+ note: "choice" in answer ? (answer.note ?? "") : "",
95
+ };
96
+ }
97
+ const refusalCodes = [
98
+ "not_granted",
99
+ "invalid_input",
100
+ "invalid_output",
101
+ "revoked",
102
+ "member_revoked",
103
+ "workflow_run_revoked",
104
+ "computer_revoked",
105
+ "budget_exhausted",
106
+ "unknown_method",
107
+ ];
108
+ export const dispatchResultSchema = S.Union([
109
+ S.Struct({
110
+ kind: S.Literal("complete"),
111
+ output: S.Json,
112
+ costMilliunits: natural,
113
+ row: S.optionalKey(resultRowSchemaV1),
114
+ }),
115
+ S.Struct({ kind: S.Literal("refused"), code: S.Literals(refusalCodes) }),
116
+ S.Struct({ kind: S.Literal("pending_approval"), actionId: id }),
117
+ /**
118
+ * The App accepted the work and it is still running. The engine calls the
119
+ * same method again with the same idempotency key after `retryAfterMs`.
120
+ */
121
+ S.Struct({
122
+ kind: S.Literal("pending"),
123
+ retryAfterMs: S.Number.pipe(S.check(S.isInt()), S.check(S.isBetween({ minimum: 1_000, maximum: 60_000 }))),
124
+ }),
125
+ ]);
126
+ /** Live admission before any node kind; refusals use the engine's sole vocabulary. */
127
+ export const nodeAdmissionResultSchema = S.Union([
128
+ S.Struct({ kind: S.Literal("admitted") }),
129
+ S.Struct({ kind: S.Literal("refused"), code: S.Literals(refusalCodes) }),
130
+ ]);
@@ -0,0 +1,16 @@
1
+ import type { Mapping, Node, Predicate, PortRef, Json } from "./schema.js";
2
+ import { type PortSchema } from "./ports.js";
3
+ export type Values = Readonly<Record<string, Readonly<Record<string, Json>>>>;
4
+ /** Expected syntax refusal only; defects in a parser or graph traversal escape. */
5
+ export declare class ExpressionSyntaxError extends Error {
6
+ }
7
+ /** Mustache parses the representation; only its interpolation tokens belong to v1. */
8
+ export declare function templateRefs(template: string): readonly PortRef[];
9
+ export declare function mappingRefs(mapping: Mapping): readonly PortRef[];
10
+ export declare function predicateRefs(test: Predicate): readonly PortRef[];
11
+ export declare function nodeRefs(node: Node): readonly PortRef[];
12
+ export declare function readPort(values: Values, ref: PortRef): Json | undefined;
13
+ /** Infer constructed data through the same Mapping grammar used at execution. */
14
+ export declare function mappingProjection(mapping: Mapping, source: (ref: PortRef, path?: string) => PortSchema | undefined): PortSchema;
15
+ export declare function evaluateMapping(mapping: Mapping, values: Values): Json | undefined;
16
+ export declare function evaluatePredicate(test: Predicate, values: Values): boolean;