@fungi.computer/braid 0.0.1 → 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.
@@ -103,6 +103,9 @@ export async function runGraphWorkflow(step, params, instanceId, workflows, disp
103
103
  throw new NonRetryableError("invalid_input");
104
104
  const input = admitted.input;
105
105
  const iteration = checked.kind === "run" ? "0" : checked.iteration;
106
+ const position = checked.kind === "run" ? [] : checked.position;
107
+ /** Steps inside a loop say which item or round they belong to. */
108
+ const placed = (record) => position.length === 0 ? record : { ...record, position };
106
109
  const values = {};
107
110
  values.$input = input;
108
111
  const baseEvent = (node, state, attempt = 0, poll) => {
@@ -116,20 +119,21 @@ export async function runGraphWorkflow(step, params, instanceId, workflows, disp
116
119
  if (poll !== undefined)
117
120
  event = { ...event, poll };
118
121
  return checked.kind === "map-item"
119
- ? { ...event, graphId: checked.body }
122
+ ? placed({ ...event, graphId: checked.body })
120
123
  : event;
121
124
  };
122
- const complete = async (node, output, attempt = 0, costMilliunits = 0, durationMs = 0, poll) => {
125
+ const complete = async (node, output, attempt = 0, costMilliunits = 0, durationMs = 0, poll, row) => {
123
126
  const ports = { ...output, after: true };
124
127
  if (exceedsResultLimit(ports))
125
128
  return fail(node, "step_result_limit", undefined, poll);
126
- await dispatcher.recordEvent({
129
+ const event = {
127
130
  ...baseEvent(node, "complete", attempt, poll),
128
131
  costMilliunits,
129
132
  durationMs,
130
133
  outputDigest: await contentDigest(ports),
131
134
  output,
132
- });
135
+ };
136
+ await dispatcher.recordEvent(row === undefined ? event : { ...event, row });
133
137
  return ports;
134
138
  };
135
139
  const fail = async (node, code, cause, poll) => {
@@ -289,7 +293,7 @@ export async function runGraphWorkflow(step, params, instanceId, workflows, disp
289
293
  if (ports === undefined)
290
294
  return fail(node, "invalid_output", undefined, poll);
291
295
  return {
292
- ports: await complete(node, ports, ctx.attempt, result.costMilliunits, Date.now() - start, poll),
296
+ ports: await complete(node, ports, ctx.attempt, result.costMilliunits, Date.now() - start, poll, result.row),
293
297
  };
294
298
  });
295
299
  // A pending App call is polled with the same idempotency key, within
@@ -331,7 +335,7 @@ export async function runGraphWorkflow(step, params, instanceId, workflows, disp
331
335
  const eventType = "review_" +
332
336
  (await contentDigest([instanceId, node.id, iteration])).slice(7);
333
337
  const expiresAt = await step.do(name + ":pending", async () => {
334
- const deadline = await dispatcher.openReview({
338
+ const deadline = await dispatcher.openReview(placed({
335
339
  runId: checked.runId,
336
340
  nodeId: node.id,
337
341
  iteration,
@@ -342,7 +346,7 @@ export async function runGraphWorkflow(step, params, instanceId, workflows, disp
342
346
  timeout: node.timeout,
343
347
  show: node.show.map((ref) => readPort(values, ref) ?? null),
344
348
  ...reviewChoices(node),
345
- });
349
+ }));
346
350
  await dispatcher.recordEvent(baseEvent(node, "waiting"));
347
351
  return deadline;
348
352
  });
@@ -404,6 +408,15 @@ export async function runGraphWorkflow(step, params, instanceId, workflows, disp
404
408
  parentInstanceId: instanceId,
405
409
  completionEvent: eventType,
406
410
  input: { item, round, previous },
411
+ position: [
412
+ ...position,
413
+ {
414
+ node: node.id,
415
+ loop: "repeat",
416
+ index: round,
417
+ count: node.maxRounds,
418
+ },
419
+ ],
407
420
  };
408
421
  try {
409
422
  await workflows.create({ id: childId, params });
@@ -467,6 +480,15 @@ export async function runGraphWorkflow(step, params, instanceId, workflows, disp
467
480
  itemIndex,
468
481
  ])).slice(7),
469
482
  input: { item, index: itemIndex },
483
+ position: [
484
+ ...position,
485
+ {
486
+ node: node.id,
487
+ loop: "map",
488
+ index: itemIndex,
489
+ count: list.length,
490
+ },
491
+ ],
470
492
  };
471
493
  try {
472
494
  await workflows.create({ id: childId, params: childParams });
package/dist/compile.d.ts CHANGED
@@ -19,6 +19,8 @@ export type DeclaredMethod = Readonly<{
19
19
  pending?: true | undefined;
20
20
  /** Discovery-only metadata from the App declaration; hidden methods have no graph ports. */
21
21
  hidden?: true | undefined;
22
+ /** Presentation is display metadata and never part of a method's graph contract. */
23
+ present?: object | undefined;
22
24
  description: string;
23
25
  input: object;
24
26
  output: object;
package/dist/compile.js CHANGED
@@ -190,6 +190,12 @@ function completionView(method) {
190
190
  })(output);
191
191
  return Option.isSome(union) ? schemaView(union.value.anyOf[0]) : output;
192
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
+ }
193
199
  /**
194
200
  * The catalog entry for one App's declared methods. The digest covers what a
195
201
  * node depends on (ports, effect, approval, requires), not the description, so
@@ -198,7 +204,9 @@ function completionView(method) {
198
204
  */
199
205
  export async function catalogMethods(methods) {
200
206
  const entries = await Promise.all(Object.entries(methods)
201
- .flatMap(([name, { hidden, ...declared }]) => hidden === true ? [] : [[name, declared]])
207
+ .flatMap(([name, declared]) => declared.hidden === true
208
+ ? []
209
+ : [[name, graphContract(declared)]])
202
210
  .map(async ([name, declared]) => {
203
211
  const method = S.decodeUnknownSync(methodSchema)({
204
212
  ...declared,
@@ -1,5 +1,25 @@
1
1
  import * as S from "effect/Schema";
2
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;
3
23
  export declare const graphParamsSchema: S.Union<readonly [S.Struct<{
4
24
  readonly kind: S.Literal<"run">;
5
25
  readonly teamId: S.String;
@@ -15,6 +35,12 @@ export declare const graphParamsSchema: S.Union<readonly [S.Struct<{
15
35
  readonly parentInstanceId: S.String;
16
36
  readonly completionEvent: S.String;
17
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
+ }>>;
18
44
  }>]>;
19
45
  export type GraphParams = typeof graphParamsSchema.Type;
20
46
  export declare const nodeEventSchema: S.Struct<{
@@ -22,6 +48,13 @@ export declare const nodeEventSchema: S.Struct<{
22
48
  readonly graphId: S.optionalKey<S.String>;
23
49
  readonly nodeId: S.String;
24
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
+ }>>>;
25
58
  readonly poll: S.optionalKey<S.Number>;
26
59
  readonly state: S.Literals<readonly ["queued", "running", "waiting", "complete", "failed", "skipped"]>;
27
60
  readonly attempt: S.Number;
@@ -31,6 +64,27 @@ export declare const nodeEventSchema: S.Struct<{
31
64
  readonly costMilliunits: S.optionalKey<S.Number>;
32
65
  readonly outputDigest: S.optionalKey<S.String>;
33
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
+ }>>;
34
88
  readonly errorMessage: S.optionalKey<S.String>;
35
89
  readonly refusal: S.optionalKey<S.String>;
36
90
  }>;
@@ -39,6 +93,13 @@ export declare const graphReviewSchema: S.Struct<{
39
93
  readonly runId: S.String;
40
94
  readonly nodeId: S.String;
41
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
+ }>>>;
42
103
  readonly instanceId: S.String;
43
104
  readonly eventType: S.String;
44
105
  readonly title: S.String;
@@ -66,6 +127,26 @@ export declare const dispatchResultSchema: S.Union<readonly [S.Struct<{
66
127
  readonly kind: S.Literal<"complete">;
67
128
  readonly output: S.Codec<S.Json, S.Json, never, never>;
68
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
+ }>>;
69
150
  }>, S.Struct<{
70
151
  readonly kind: S.Literal<"refused">;
71
152
  readonly code: S.Literals<readonly ["not_granted", "invalid_input", "invalid_output", "revoked", "member_revoked", "workflow_run_revoked", "computer_revoked", "budget_exhausted", "unknown_method"]>;
package/dist/execution.js CHANGED
@@ -1,6 +1,20 @@
1
1
  import * as S from "effect/Schema";
2
+ import { resultRowSchemaV1 } from "@fungi.computer/gills";
2
3
  const id = S.String.pipe(S.check(S.isMinLength(1)), S.check(S.isMaxLength(256)));
3
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));
4
18
  export const graphParamsSchema = S.Union([
5
19
  S.Struct({ kind: S.Literal("run"), teamId: id, runId: id, planDigest: id }),
6
20
  S.Struct({
@@ -13,6 +27,7 @@ export const graphParamsSchema = S.Union([
13
27
  parentInstanceId: id,
14
28
  completionEvent: id,
15
29
  input: S.JsonObject,
30
+ position: loopPositionSchema,
16
31
  }),
17
32
  ]);
18
33
  export const nodeEventSchema = S.Struct({
@@ -20,6 +35,8 @@ export const nodeEventSchema = S.Struct({
20
35
  graphId: S.optionalKey(id),
21
36
  nodeId: id,
22
37
  iteration: id,
38
+ /** The loops around this step; absent at the top of the run. */
39
+ position: S.optionalKey(loopPositionSchema),
23
40
  poll: S.optionalKey(natural),
24
41
  state: S.Literals([
25
42
  "queued",
@@ -36,6 +53,8 @@ export const nodeEventSchema = S.Struct({
36
53
  costMilliunits: S.optionalKey(natural),
37
54
  outputDigest: S.optionalKey(id),
38
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),
39
58
  errorMessage: S.optionalKey(S.String.check(S.isMaxLength(1024))),
40
59
  refusal: S.optionalKey(id),
41
60
  });
@@ -43,6 +62,8 @@ export const graphReviewSchema = S.Struct({
43
62
  runId: id,
44
63
  nodeId: id,
45
64
  iteration: id,
65
+ /** The loops around this review; absent at the top of the run. */
66
+ position: S.optionalKey(loopPositionSchema),
46
67
  instanceId: id,
47
68
  eventType: id,
48
69
  title: S.String,
@@ -89,6 +110,7 @@ export const dispatchResultSchema = S.Union([
89
110
  kind: S.Literal("complete"),
90
111
  output: S.Json,
91
112
  costMilliunits: natural,
113
+ row: S.optionalKey(resultRowSchemaV1),
92
114
  }),
93
115
  S.Struct({ kind: S.Literal("refused"), code: S.Literals(refusalCodes) }),
94
116
  S.Struct({ kind: S.Literal("pending_approval"), actionId: id }),
@@ -3,46 +3,46 @@ import { z } from "zod";
3
3
  export declare const workflowBindingSchema: z.ZodUnion<readonly [z.ZodObject<{
4
4
  appId: z.ZodString;
5
5
  installationId: z.ZodString;
6
- }, z.core.$strict>, z.ZodObject<{
6
+ }, z.core.$strict>, z.ZodLazy<z.ZodObject<{
7
7
  kind: z.ZodLiteral<"team-source">;
8
- }, z.core.$strict>]>;
8
+ }, z.core.$strict>>]>;
9
9
  /** Installation selections are admitted through the host's catalog before compilation. */
10
- export declare const bindingsSelectionSchema: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
10
+ export declare const bindingsSelectionSchema: z.ZodRecord<z.ZodLazy<z.ZodString>, z.ZodUnion<readonly [z.ZodString, z.ZodLazy<z.ZodObject<{
11
11
  kind: z.ZodLiteral<"team-source">;
12
- }, z.core.$strict>]>>;
12
+ }, z.core.$strict>>]>>;
13
13
  export type BindingsSelection = z.output<typeof bindingsSelectionSchema>;
14
14
  /** Published node declaration: a write branch ceiling or a read-only scope. */
15
15
  export declare const gitAccessSchema: z.ZodUnion<readonly [z.ZodObject<{
16
- binding: z.ZodString;
16
+ binding: z.ZodLazy<z.ZodString>;
17
17
  repository: z.ZodString;
18
- branches: z.ZodArray<z.ZodString>;
18
+ branches: z.ZodLazy<z.ZodArray<z.ZodString>>;
19
19
  }, z.core.$strict>, z.ZodObject<{
20
- binding: z.ZodString;
20
+ binding: z.ZodLazy<z.ZodString>;
21
21
  repository: z.ZodUnion<readonly [z.ZodString, z.ZodLiteral<"$trigger.repository">]>;
22
22
  access: z.ZodLiteral<"read">;
23
23
  }, z.core.$strict>]>;
24
24
  /** Host-resolved scope carried to a process; data alone never grants this authority. */
25
- export declare const resolvedGitAccessSchema: z.ZodUnion<readonly [z.ZodObject<{
26
- repository: z.ZodDiscriminatedUnion<[z.ZodObject<{
25
+ export declare const resolvedGitAccessSchema: z.ZodLazy<z.ZodUnion<readonly [z.ZodObject<{
26
+ repository: z.ZodLazy<z.ZodDiscriminatedUnion<[z.ZodObject<{
27
27
  kind: z.ZodLiteral<"installation">;
28
28
  installationId: z.ZodString;
29
29
  repository: z.ZodString;
30
30
  }, z.core.$strict>, z.ZodObject<{
31
31
  kind: z.ZodLiteral<"team-source">;
32
32
  repository: z.ZodString;
33
- }, z.core.$strict>], "kind">;
34
- branches: z.ZodArray<z.ZodString>;
33
+ }, z.core.$strict>], "kind">>;
34
+ branches: z.ZodLazy<z.ZodArray<z.ZodString>>;
35
35
  }, z.core.$strict>, z.ZodObject<{
36
- repository: z.ZodDiscriminatedUnion<[z.ZodObject<{
36
+ repository: z.ZodLazy<z.ZodDiscriminatedUnion<[z.ZodObject<{
37
37
  kind: z.ZodLiteral<"installation">;
38
38
  installationId: z.ZodString;
39
39
  repository: z.ZodString;
40
40
  }, z.core.$strict>, z.ZodObject<{
41
41
  kind: z.ZodLiteral<"team-source">;
42
42
  repository: z.ZodString;
43
- }, z.core.$strict>], "kind">;
43
+ }, z.core.$strict>], "kind">>;
44
44
  access: z.ZodLiteral<"read">;
45
- }, z.core.$strict>]>;
45
+ }, z.core.$strict>]>>;
46
46
  export type GitAccess = z.infer<typeof gitAccessSchema>;
47
47
  export type ResolvedGitAccess = z.infer<typeof resolvedGitAccessSchema>;
48
48
  /** The Git scope a pass for this resolved access admits. */
@@ -4,13 +4,13 @@ const repository = z
4
4
  .string()
5
5
  .regex(/^[a-zA-Z0-9._-]{1,128}$/)
6
6
  .refine((value) => value !== "." && value !== "..");
7
- const bindingName = z
7
+ const bindingName = z.lazy(() => z
8
8
  .string()
9
9
  .min(1)
10
10
  .max(80)
11
- .regex(/^[A-Za-z][A-Za-z0-9_-]*$/);
11
+ .regex(/^[A-Za-z][A-Za-z0-9_-]*$/));
12
12
  const identity = z.string().min(1).max(256);
13
- const teamSourceBinding = z.strictObject({ kind: z.literal("team-source") });
13
+ const teamSourceBinding = z.lazy(() => z.strictObject({ kind: z.literal("team-source") }));
14
14
  /** Frozen installation identity or explicit registered Team-source namespace. */
15
15
  export const workflowBindingSchema = z.union([
16
16
  z.strictObject({ appId: identity, installationId: identity }),
@@ -30,11 +30,11 @@ const branch = z
30
30
  value
31
31
  .split("/")
32
32
  .every((part) => !part.startsWith(".") && !part.endsWith(".lock")));
33
- const branches = z
33
+ const branches = z.lazy(() => z
34
34
  .array(branch)
35
35
  .min(1)
36
36
  .max(256)
37
- .refine((values) => new Set(values).size === values.length, "Branches must be unique");
37
+ .refine((values) => new Set(values).size === values.length, "Branches must be unique"));
38
38
  /** Write access names a fixed repository and its exact branch ceiling, admitted at arm time. */
39
39
  const writeGitAccessSchema = z.strictObject({
40
40
  binding: bindingName,
@@ -57,13 +57,13 @@ export const gitAccessSchema = z.union([
57
57
  readGitAccessSchema,
58
58
  ]);
59
59
  /** Host-resolved scope carried to a process; data alone never grants this authority. */
60
- export const resolvedGitAccessSchema = z.union([
60
+ export const resolvedGitAccessSchema = z.lazy(() => z.union([
61
61
  z.strictObject({ repository: repositoryRefSchema, branches }),
62
62
  z.strictObject({
63
63
  repository: repositoryRefSchema,
64
64
  access: z.literal("read"),
65
65
  }),
66
- ]);
66
+ ]));
67
67
  /** The Git scope a pass for this resolved access admits. */
68
68
  export function gitAccessScope(access) {
69
69
  return "access" in access ? access.access : "write";
package/dist/index.d.ts CHANGED
@@ -6,3 +6,4 @@ export * from "./compile.js";
6
6
  export * from "./triggers.js";
7
7
  export * from "./execution.js";
8
8
  export * from "./workflow-id.js";
9
+ export * from "./review-text.js";
package/dist/index.js CHANGED
@@ -6,3 +6,4 @@ export * from "./compile.js";
6
6
  export * from "./triggers.js";
7
7
  export * from "./execution.js";
8
8
  export * from "./workflow-id.js";
9
+ export * from "./review-text.js";
@@ -0,0 +1,15 @@
1
+ import type { LoopPosition } from "./execution.js";
2
+ /**
3
+ * The words a review's `detail` and choice descriptions may ask for, filled
4
+ * from the loops around the review: the innermost Map gives the item, the
5
+ * innermost Repeat the try. Each counts from 1, as a person does.
6
+ */
7
+ export declare const reviewTextPlaceholders: readonly ["item", "items", "try", "tries"];
8
+ /** Placeholder names in `text` that no review can fill. */
9
+ export declare function unknownReviewPlaceholders(text: string): string[];
10
+ /**
11
+ * Fill a review's text from where it runs. Text whose placeholders the
12
+ * position cannot fill (a `{try}` outside any Repeat) is not shown at all,
13
+ * rather than shown with a hole or a guess.
14
+ */
15
+ export declare function fillReviewText(text: string, position?: LoopPosition): string | undefined;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The words a review's `detail` and choice descriptions may ask for, filled
3
+ * from the loops around the review: the innermost Map gives the item, the
4
+ * innermost Repeat the try. Each counts from 1, as a person does.
5
+ */
6
+ export const reviewTextPlaceholders = [
7
+ "item",
8
+ "items",
9
+ "try",
10
+ "tries",
11
+ ];
12
+ const placeholder = /\{([A-Za-z]+)\}/g;
13
+ const known = (name) => reviewTextPlaceholders.some((candidate) => candidate === name);
14
+ /** Placeholder names in `text` that no review can fill. */
15
+ export function unknownReviewPlaceholders(text) {
16
+ return [...text.matchAll(placeholder)].flatMap(([, name = ""]) => known(name) ? [] : [name]);
17
+ }
18
+ /**
19
+ * Fill a review's text from where it runs. Text whose placeholders the
20
+ * position cannot fill (a `{try}` outside any Repeat) is not shown at all,
21
+ * rather than shown with a hole or a guess.
22
+ */
23
+ export function fillReviewText(text, position = []) {
24
+ const map = position.findLast((place) => place.loop === "map");
25
+ const repeat = position.findLast((place) => place.loop === "repeat");
26
+ const values = {
27
+ item: map === undefined ? undefined : map.index + 1,
28
+ items: map?.count,
29
+ try: repeat === undefined ? undefined : repeat.index + 1,
30
+ tries: repeat?.count,
31
+ };
32
+ const valueOf = (name) => (known(name) ? values[name] : undefined);
33
+ const names = [...text.matchAll(placeholder)].map(([, name = ""]) => name);
34
+ if (names.some((name) => valueOf(name) === undefined))
35
+ return undefined;
36
+ return text.replace(placeholder, (whole, name) => {
37
+ const value = valueOf(name);
38
+ return value === undefined ? whole : String(value);
39
+ });
40
+ }
@@ -15,7 +15,7 @@ export declare const workflowArmedViaSchema: z.ZodDiscriminatedUnion<[z.ZodObjec
15
15
  /** Provenance from the actual connector or approved-policy owner. */
16
16
  export type WorkflowArmedVia = z.output<typeof workflowArmedViaSchema>;
17
17
  /** The grantor stays the armer; decoding this shape alone grants no authority. */
18
- export declare const workflowArmingReceiptSchema: z.ZodObject<{
18
+ export declare const workflowArmingReceiptSchema: z.ZodLazy<z.ZodObject<{
19
19
  armedBy: z.ZodString;
20
20
  armedVia: z.ZodNullable<z.ZodDiscriminatedUnion<[z.ZodObject<{
21
21
  kind: z.ZodLiteral<"connector">;
@@ -28,12 +28,12 @@ export declare const workflowArmingReceiptSchema: z.ZodObject<{
28
28
  policyRevision: z.ZodString;
29
29
  grantorMemberId: z.ZodString;
30
30
  }, z.core.$strict>], "kind">>;
31
- grantorMembershipVersion: z.ZodNumber;
32
- }, z.core.$strict>;
31
+ grantorMembershipVersion: z.ZodLazy<z.ZodNumber>;
32
+ }, z.core.$strict>>;
33
33
  /** Captured by a host after current frame, installation and member admission. */
34
34
  export type WorkflowArmingReceipt = z.output<typeof workflowArmingReceiptSchema>;
35
35
  /** Current host-owner readback, after live grantor and approval checks. */
36
- export declare const workflowArmingAccessSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
36
+ export declare const workflowArmingAccessSchema: z.ZodLazy<z.ZodDiscriminatedUnion<[z.ZodObject<{
37
37
  kind: z.ZodLiteral<"connector">;
38
38
  agentId: z.ZodString;
39
39
  attachmentId: z.ZodString;
@@ -52,7 +52,7 @@ export declare const workflowArmingAccessSchema: z.ZodDiscriminatedUnion<[z.ZodO
52
52
  write: "write";
53
53
  read: "read";
54
54
  }>;
55
- }, z.core.$strict>], "kind">;
55
+ }, z.core.$strict>], "kind">>;
56
56
  /** The actual source identity plus its currently approved access level. */
57
57
  export type WorkflowArmingAccess = z.output<typeof workflowArmingAccessSchema>;
58
58
  /** Lookup keys exclude captured versions, revisions and grantors. */
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { decodeWorkflowRunMember } from "./run-member.js";
3
3
  const identity = z.string().min(1).max(256);
4
- const membershipVersion = z.number().int().nonnegative();
4
+ const membershipVersion = z.lazy(() => z.number().int().nonnegative());
5
5
  const connector = z.strictObject({
6
6
  kind: z.literal("connector"),
7
7
  agentId: identity,
@@ -21,7 +21,7 @@ export const workflowArmedViaSchema = z.discriminatedUnion("kind", [
21
21
  policy,
22
22
  ]);
23
23
  /** The grantor stays the armer; decoding this shape alone grants no authority. */
24
- export const workflowArmingReceiptSchema = z
24
+ export const workflowArmingReceiptSchema = z.lazy(() => z
25
25
  .strictObject({
26
26
  armedBy: identity,
27
27
  /** A human arm has an explicit null; Agent arms retain their actual live source. */
@@ -36,15 +36,15 @@ export const workflowArmingReceiptSchema = z
36
36
  path: ["armedVia", "grantorMemberId"],
37
37
  message: "The approved policy grantor must be the armer",
38
38
  });
39
- });
39
+ }));
40
40
  /** Current host-owner readback, after live grantor and approval checks. */
41
- export const workflowArmingAccessSchema = z.discriminatedUnion("kind", [
41
+ export const workflowArmingAccessSchema = z.lazy(() => z.discriminatedUnion("kind", [
42
42
  connector.extend({
43
43
  grantorMemberId: identity,
44
44
  level: z.enum(["read", "write"]),
45
45
  }),
46
46
  policy.extend({ level: z.enum(["read", "write"]) }),
47
- ]);
47
+ ]));
48
48
  /** Recheck Agent authority only; the host also admits member, Computer and installation grants. */
49
49
  export async function recheckWorkflowArming(receipt, ports) {
50
50
  const captured = workflowArmingReceiptSchema.safeParse(receipt);
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import type { GitWriter } from "@fungi.computer/agar/git";
3
3
  /** Non-secret Team-issued authority for one stored Workflow step on its pinned Computer. */
4
- export declare const workflowGitPassGrantSchema: z.ZodObject<{
4
+ export declare const workflowGitPassGrantSchema: z.ZodLazy<z.ZodObject<{
5
5
  teamId: z.ZodString;
6
6
  computerId: z.ZodString;
7
7
  memberId: z.ZodString;
@@ -28,29 +28,29 @@ export declare const workflowGitPassGrantSchema: z.ZodObject<{
28
28
  write: "write";
29
29
  read: "read";
30
30
  }>;
31
- gitAccess: z.ZodUnion<readonly [z.ZodObject<{
32
- repository: z.ZodDiscriminatedUnion<[z.ZodObject<{
31
+ gitAccess: z.ZodLazy<z.ZodUnion<readonly [z.ZodObject<{
32
+ repository: z.ZodLazy<z.ZodDiscriminatedUnion<[z.ZodObject<{
33
33
  kind: z.ZodLiteral<"installation">;
34
34
  installationId: z.ZodString;
35
35
  repository: z.ZodString;
36
36
  }, z.core.$strict>, z.ZodObject<{
37
37
  kind: z.ZodLiteral<"team-source">;
38
38
  repository: z.ZodString;
39
- }, z.core.$strict>], "kind">;
40
- branches: z.ZodArray<z.ZodString>;
39
+ }, z.core.$strict>], "kind">>;
40
+ branches: z.ZodLazy<z.ZodArray<z.ZodString>>;
41
41
  }, z.core.$strict>, z.ZodObject<{
42
- repository: z.ZodDiscriminatedUnion<[z.ZodObject<{
42
+ repository: z.ZodLazy<z.ZodDiscriminatedUnion<[z.ZodObject<{
43
43
  kind: z.ZodLiteral<"installation">;
44
44
  installationId: z.ZodString;
45
45
  repository: z.ZodString;
46
46
  }, z.core.$strict>, z.ZodObject<{
47
47
  kind: z.ZodLiteral<"team-source">;
48
48
  repository: z.ZodString;
49
- }, z.core.$strict>], "kind">;
49
+ }, z.core.$strict>], "kind">>;
50
50
  access: z.ZodLiteral<"read">;
51
- }, z.core.$strict>]>;
51
+ }, z.core.$strict>]>>;
52
52
  triggerId: z.ZodOptional<z.ZodString>;
53
- }, z.core.$strict>;
53
+ }, z.core.$strict>>;
54
54
  export type WorkflowGitPassGrant = z.output<typeof workflowGitPassGrantSchema>;
55
55
  /** Run cleanup remains valid after the Computer's product data is erased. */
56
56
  export declare const workflowGitPassRevocationSchema: z.ZodObject<{
@@ -3,7 +3,7 @@ import { workflowAttestationSchema } from "@fungi.computer/puffball/app-method-r
3
3
  import { gitAccessScope, resolvedGitAccessSchema } from "../git-access.js";
4
4
  const id = z.string().min(1).max(256);
5
5
  /** Non-secret Team-issued authority for one stored Workflow step on its pinned Computer. */
6
- export const workflowGitPassGrantSchema = z
6
+ export const workflowGitPassGrantSchema = z.lazy(() => z
7
7
  .strictObject({
8
8
  teamId: id,
9
9
  computerId: id,
@@ -17,7 +17,7 @@ export const workflowGitPassGrantSchema = z
17
17
  gitAccess: resolvedGitAccessSchema,
18
18
  triggerId: id.optional(),
19
19
  })
20
- .refine((grant) => grant.scope === gitAccessScope(grant.gitAccess), "A pass's scope is its admitted Git access");
20
+ .refine((grant) => grant.scope === gitAccessScope(grant.gitAccess), "A pass's scope is its admitted Git access"));
21
21
  /** Run cleanup remains valid after the Computer's product data is erased. */
22
22
  export const workflowGitPassRevocationSchema = z.strictObject({
23
23
  teamId: id,