@fungi.computer/braid 0.0.1 → 0.2.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 (39) hide show
  1. package/dist/cloudflare/graph-program.js +29 -7
  2. package/dist/code-execution.js +2 -0
  3. package/dist/compile.d.ts +2 -0
  4. package/dist/compile.js +9 -1
  5. package/dist/execution.d.ts +81 -0
  6. package/dist/execution.js +22 -0
  7. package/dist/git-access.d.ts +15 -15
  8. package/dist/git-access.js +8 -8
  9. package/dist/index.d.ts +1 -0
  10. package/dist/index.js +1 -0
  11. package/dist/review-text.d.ts +15 -0
  12. package/dist/review-text.js +40 -0
  13. package/dist/runtime/arming-authority.d.ts +5 -5
  14. package/dist/runtime/arming-authority.js +5 -5
  15. package/dist/runtime/git-pass-contract.d.ts +30 -9
  16. package/dist/runtime/git-pass-contract.js +26 -2
  17. package/dist/runtime/git-pass-store.d.ts +16 -13
  18. package/dist/runtime/git-pass-store.js +126 -50
  19. package/dist/runtime/git-pass.d.ts +3 -4
  20. package/dist/runtime/git-pass.js +3 -17
  21. package/dist/runtime/graph-contract.d.ts +5 -1
  22. package/dist/runtime/graph-run-store.d.ts +6 -0
  23. package/dist/runtime/ports.d.ts +11 -1
  24. package/dist/runtime/run-ceiling.d.ts +1 -1
  25. package/dist/runtime/run-owner.d.ts +89 -4
  26. package/dist/runtime/run-owner.js +27 -12
  27. package/dist/runtime/schedules.d.ts +1 -0
  28. package/dist/runtime/schedules.js +1 -0
  29. package/dist/runtime/trigger-owner.d.ts +10 -6
  30. package/dist/runtime/trigger-owner.js +2 -2
  31. package/dist/runtime/trigger-plans.d.ts +8 -0
  32. package/dist/runtime/trigger-run-reservations.d.ts +67 -3
  33. package/dist/runtime/trigger-run-reservations.js +126 -7
  34. package/dist/schema.d.ts +80 -8
  35. package/dist/schema.js +14 -2
  36. package/dist/validate.js +7 -0
  37. package/dist/workflow-id.d.ts +1 -1
  38. package/dist/workflow-id.js +1 -1
  39. package/package.json +8 -7
@@ -8,22 +8,22 @@ const reservationIds = defineIdFamily({
8
8
  kinds: { reservation: { prefix: "wf_trigger_reservation" } },
9
9
  });
10
10
  const identity = z.string().min(1).max(256);
11
- const triggerRunScopeSchema = z.strictObject({
11
+ const triggerRunScopeSchema = z.lazy(() => z.strictObject({
12
12
  installationId: identity,
13
13
  workflowId: workflowIdSchema,
14
14
  commit: gitSha1Schema,
15
15
  triggerId: z.string().min(1).max(80),
16
- });
17
- const reservationInputSchema = z.strictObject({
16
+ }));
17
+ const reservationInputSchema = z.lazy(() => z.strictObject({
18
18
  scope: triggerRunScopeSchema,
19
19
  requestId: z.string().min(1).max(1024),
20
- });
21
- const rowSchema = z.object({
20
+ }));
21
+ const rowSchema = z.lazy(() => z.object({
22
22
  reservation_id: identity,
23
23
  run_id: identity.nullable(),
24
24
  status: z.enum(["active", "released", "terminal"]),
25
- });
26
- const countSchema = z.object({ count: z.number().int().nonnegative() });
25
+ }));
26
+ const countSchema = z.lazy(() => z.object({ count: z.number().int().nonnegative() }));
27
27
  const scopeKey = (scope) => {
28
28
  const parsed = triggerRunScopeSchema.parse(scope);
29
29
  return JSON.stringify([
@@ -33,15 +33,134 @@ const scopeKey = (scope) => {
33
33
  parsed.triggerId,
34
34
  ]);
35
35
  };
36
+ const deliveryIdentitySchema = z.strictObject({
37
+ host: identity,
38
+ tenant: identity,
39
+ bindingId: identity,
40
+ eventId: identity,
41
+ });
42
+ const deliveryInputSchema = z.strictObject({
43
+ scope: triggerRunScopeSchema,
44
+ delivery: deliveryIdentitySchema,
45
+ cargo: z.json(),
46
+ limits: z.strictObject({
47
+ hostConcurrency: z.number().int().positive().max(20),
48
+ bindingConcurrency: z.number().int().positive().max(20),
49
+ }),
50
+ });
51
+ const deliveryOwnerSchema = deliveryIdentitySchema.pick({
52
+ host: true,
53
+ tenant: true,
54
+ });
55
+ const pendingDeliverySchema = rowSchema.and(z.object({
56
+ scope: z.string(),
57
+ delivery_record: z.string().nullable(),
58
+ }));
59
+ const DAY = 24 * 60 * 60 * 1000;
36
60
  /** The enclosing Team owner supplies SQLite; callers never select the Team. */
37
61
  export function createTriggerRunReservations(options) {
38
62
  const { sql } = options;
63
+ const now = options.now ?? Date.now;
39
64
  sql.exec(`CREATE TABLE IF NOT EXISTS hub_trigger_run_reservations (
40
65
  reservation_id TEXT PRIMARY KEY, scope TEXT NOT NULL, request_id TEXT NOT NULL,
41
66
  run_id TEXT UNIQUE, status TEXT NOT NULL CHECK(status IN ('active','released','terminal')),
67
+ delivery_key TEXT, delivery_record TEXT, delivery_expires_at INTEGER,
68
+ delivery_host TEXT, delivery_tenant TEXT, delivery_binding TEXT,
69
+ delivery_pending INTEGER NOT NULL DEFAULT 0 CHECK(delivery_pending IN (0,1)),
42
70
  UNIQUE(scope,request_id)
43
71
  ) STRICT`);
72
+ sql.exec(`CREATE INDEX IF NOT EXISTS hub_trigger_delivery_identity
73
+ ON hub_trigger_run_reservations(delivery_key,delivery_expires_at)`);
74
+ sql.exec(`CREATE INDEX IF NOT EXISTS hub_trigger_delivery_expiry
75
+ ON hub_trigger_run_reservations(delivery_expires_at)`);
76
+ const expireDeliveries = () => {
77
+ const at = now();
78
+ // An expired unstarted intent cannot wake forever. A recorded active run
79
+ // still owns its concurrency slot even after delivery deduplication expires.
80
+ sql.exec("UPDATE hub_trigger_run_reservations SET status='terminal',delivery_pending=0 WHERE reservation_id IN (SELECT reservation_id FROM hub_trigger_run_reservations WHERE delivery_expires_at<=? AND run_id IS NULL LIMIT 100)", at);
81
+ sql.exec("DELETE FROM hub_trigger_run_reservations WHERE reservation_id IN (SELECT reservation_id FROM hub_trigger_run_reservations WHERE delivery_expires_at<=? AND status!='active' AND delivery_pending=0 LIMIT 100)", at);
82
+ sql.exec("UPDATE hub_trigger_run_reservations SET delivery_key=NULL,delivery_record=NULL,delivery_expires_at=NULL WHERE reservation_id IN (SELECT reservation_id FROM hub_trigger_run_reservations WHERE delivery_expires_at<=? AND run_id IS NOT NULL LIMIT 100)", at);
83
+ sql.exec("DELETE FROM hub_trigger_run_reservations WHERE reservation_id IN (SELECT reservation_id FROM hub_trigger_run_reservations WHERE delivery_host IS NOT NULL AND delivery_expires_at IS NULL AND status!='active' AND delivery_pending=0 LIMIT 100)");
84
+ };
44
85
  return {
86
+ /** Opaque verified cargo and delivery identity live with the run reservation. */
87
+ reserveWebhookDelivery(raw) {
88
+ const input = deliveryInputSchema.parse(raw);
89
+ const key = JSON.stringify(input.delivery);
90
+ return options.transactionSync(() => {
91
+ expireDeliveries();
92
+ const prior = sql
93
+ .exec("SELECT reservation_id,run_id,status FROM hub_trigger_run_reservations WHERE delivery_key=? AND delivery_expires_at>? ORDER BY delivery_expires_at DESC LIMIT 1", key, now())
94
+ .toArray()[0];
95
+ if (prior !== undefined) {
96
+ const row = rowSchema.parse(prior);
97
+ return {
98
+ kind: "replay",
99
+ reservationId: row.reservation_id,
100
+ runId: row.run_id,
101
+ };
102
+ }
103
+ const active = z
104
+ .object({
105
+ count: z.number(),
106
+ binding_count: z.number(),
107
+ })
108
+ .parse(sql
109
+ .exec("SELECT count(*) AS count,coalesce(sum(delivery_host=? AND delivery_tenant=? AND delivery_binding=?),0) AS binding_count FROM hub_trigger_run_reservations WHERE status='active'", input.delivery.host, input.delivery.tenant, input.delivery.bindingId)
110
+ .toArray()[0]);
111
+ if (active.count >= input.limits.hostConcurrency ||
112
+ active.binding_count >= input.limits.bindingConcurrency)
113
+ return { kind: "refused", reason: "limit" };
114
+ const reservationId = reservationIds.reservation.generate();
115
+ sql.exec("INSERT INTO hub_trigger_run_reservations(reservation_id,scope,request_id,run_id,status,delivery_key,delivery_record,delivery_expires_at,delivery_pending,delivery_host,delivery_tenant,delivery_binding) VALUES(?,?,?,NULL,'active',?,?,?,1,?,?,?)", reservationId, scopeKey(input.scope), reservationId, key, JSON.stringify(input), now() + DAY, input.delivery.host, input.delivery.tenant, input.delivery.bindingId);
116
+ return { kind: "reserved", reservationId, runId: null };
117
+ });
118
+ },
119
+ /** Bounded restart/lost-launch-reply recovery over the SAME reservations. */
120
+ pendingWebhookDeliveries(rawOwner) {
121
+ const owner = deliveryOwnerSchema.parse(rawOwner);
122
+ return options.transactionSync(() => {
123
+ expireDeliveries();
124
+ return sql
125
+ .exec("SELECT reservation_id,run_id,status,scope,delivery_record FROM hub_trigger_run_reservations WHERE delivery_host=? AND delivery_tenant=? AND delivery_pending=1 ORDER BY delivery_expires_at LIMIT 32", owner.host, owner.tenant)
126
+ .toArray()
127
+ .map((raw) => {
128
+ const row = pendingDeliverySchema.parse(raw);
129
+ if (row.run_id !== null)
130
+ return {
131
+ kind: "launch",
132
+ reservationId: row.reservation_id,
133
+ runId: row.run_id,
134
+ };
135
+ const record = deliveryInputSchema.parse(JSON.parse(z.string().parse(row.delivery_record)));
136
+ return {
137
+ kind: "start",
138
+ reservationId: row.reservation_id,
139
+ ...record,
140
+ };
141
+ });
142
+ });
143
+ },
144
+ /** Native alarms share this deadline with schedules, including idle TTL cleanup. */
145
+ nextWebhookWake(rawOwner) {
146
+ const owner = deliveryOwnerSchema.parse(rawOwner);
147
+ return options.transactionSync(() => {
148
+ expireDeliveries();
149
+ const row = z
150
+ .object({
151
+ pending: z.number(),
152
+ expiry: z.number().nullable(),
153
+ })
154
+ .parse(sql
155
+ .exec("SELECT coalesce(max(delivery_pending),0) AS pending,min(delivery_expires_at) AS expiry FROM hub_trigger_run_reservations WHERE delivery_host=? AND delivery_tenant=?", owner.host, owner.tenant)
156
+ .toArray()[0]);
157
+ return row.pending === 1 ? now() + 1000 : row.expiry;
158
+ });
159
+ },
160
+ completeWebhookDelivery(rawReservationId) {
161
+ const reservationId = identity.parse(rawReservationId);
162
+ sql.exec("UPDATE hub_trigger_run_reservations SET delivery_pending=0,delivery_record=NULL,status=CASE WHEN run_id IS NULL THEN 'terminal' ELSE status END WHERE reservation_id=? AND delivery_pending=1", reservationId);
163
+ },
45
164
  reserveTriggerRun(raw) {
46
165
  const input = reservationInputSchema.parse(raw);
47
166
  const scope = scopeKey(input.scope);
package/dist/schema.d.ts CHANGED
@@ -94,6 +94,12 @@ export declare const nodeSchema: S.Union<readonly [S.Struct<{
94
94
  readonly id: S.String;
95
95
  readonly kind: S.Literal<"review">;
96
96
  readonly title: S.String;
97
+ /**
98
+ * One plain line under the title saying where the run is, such as
99
+ * `Beat {item} of {items} · try {try} of {tries}`. The placeholders are
100
+ * filled from the loops around the review (see `fillReviewText`).
101
+ */
102
+ readonly detail: S.optionalKey<S.String>;
97
103
  readonly show: S.$Array<S.Struct<{
98
104
  readonly node: S.Union<readonly [S.String, S.Literals<readonly ["$input", "$item"]>]>;
99
105
  readonly port: S.String;
@@ -102,11 +108,14 @@ export declare const nodeSchema: S.Union<readonly [S.Struct<{
102
108
  readonly timeout: S.Number;
103
109
  /**
104
110
  * Named outcomes a reviewer picks from; each is an output port carrying
105
- * the shown item. Without choices the review approves or rejects.
111
+ * the shown item. Without choices the review approves or rejects. A
112
+ * choice's `description` says in plain words what picking it does, with
113
+ * the same placeholders as `detail`.
106
114
  */
107
115
  readonly choices: S.optionalKey<S.$Array<S.Struct<{
108
116
  readonly id: S.String;
109
117
  readonly label: S.String;
118
+ readonly description: S.optionalKey<S.String>;
110
119
  }>>>;
111
120
  }>, S.Struct<{
112
121
  readonly id: S.String;
@@ -199,6 +208,12 @@ export declare const graphSchema: S.Struct<{
199
208
  readonly id: S.String;
200
209
  readonly kind: S.Literal<"review">;
201
210
  readonly title: S.String;
211
+ /**
212
+ * One plain line under the title saying where the run is, such as
213
+ * `Beat {item} of {items} · try {try} of {tries}`. The placeholders are
214
+ * filled from the loops around the review (see `fillReviewText`).
215
+ */
216
+ readonly detail: S.optionalKey<S.String>;
202
217
  readonly show: S.$Array<S.Struct<{
203
218
  readonly node: S.Union<readonly [S.String, S.Literals<readonly ["$input", "$item"]>]>;
204
219
  readonly port: S.String;
@@ -207,11 +222,14 @@ export declare const graphSchema: S.Struct<{
207
222
  readonly timeout: S.Number;
208
223
  /**
209
224
  * Named outcomes a reviewer picks from; each is an output port carrying
210
- * the shown item. Without choices the review approves or rejects.
225
+ * the shown item. Without choices the review approves or rejects. A
226
+ * choice's `description` says in plain words what picking it does, with
227
+ * the same placeholders as `detail`.
211
228
  */
212
229
  readonly choices: S.optionalKey<S.$Array<S.Struct<{
213
230
  readonly id: S.String;
214
231
  readonly label: S.String;
232
+ readonly description: S.optionalKey<S.String>;
215
233
  }>>>;
216
234
  }>, S.Struct<{
217
235
  readonly id: S.String;
@@ -306,6 +324,12 @@ export declare const workflowDefinitionSchema: S.Struct<{
306
324
  readonly id: S.String;
307
325
  readonly kind: S.Literal<"review">;
308
326
  readonly title: S.String;
327
+ /**
328
+ * One plain line under the title saying where the run is, such as
329
+ * `Beat {item} of {items} · try {try} of {tries}`. The placeholders are
330
+ * filled from the loops around the review (see `fillReviewText`).
331
+ */
332
+ readonly detail: S.optionalKey<S.String>;
309
333
  readonly show: S.$Array<S.Struct<{
310
334
  readonly node: S.Union<readonly [S.String, S.Literals<readonly ["$input", "$item"]>]>;
311
335
  readonly port: S.String;
@@ -314,11 +338,14 @@ export declare const workflowDefinitionSchema: S.Struct<{
314
338
  readonly timeout: S.Number;
315
339
  /**
316
340
  * Named outcomes a reviewer picks from; each is an output port carrying
317
- * the shown item. Without choices the review approves or rejects.
341
+ * the shown item. Without choices the review approves or rejects. A
342
+ * choice's `description` says in plain words what picking it does, with
343
+ * the same placeholders as `detail`.
318
344
  */
319
345
  readonly choices: S.optionalKey<S.$Array<S.Struct<{
320
346
  readonly id: S.String;
321
347
  readonly label: S.String;
348
+ readonly description: S.optionalKey<S.String>;
322
349
  }>>>;
323
350
  }>, S.Struct<{
324
351
  readonly id: S.String;
@@ -434,6 +461,12 @@ export declare const workflowDefinitionSchema: S.Struct<{
434
461
  readonly id: S.String;
435
462
  readonly kind: S.Literal<"review">;
436
463
  readonly title: S.String;
464
+ /**
465
+ * One plain line under the title saying where the run is, such as
466
+ * `Beat {item} of {items} · try {try} of {tries}`. The placeholders are
467
+ * filled from the loops around the review (see `fillReviewText`).
468
+ */
469
+ readonly detail: S.optionalKey<S.String>;
437
470
  readonly show: S.$Array<S.Struct<{
438
471
  readonly node: S.Union<readonly [S.String, S.Literals<readonly ["$input", "$item"]>]>;
439
472
  readonly port: S.String;
@@ -442,11 +475,14 @@ export declare const workflowDefinitionSchema: S.Struct<{
442
475
  readonly timeout: S.Number;
443
476
  /**
444
477
  * Named outcomes a reviewer picks from; each is an output port carrying
445
- * the shown item. Without choices the review approves or rejects.
478
+ * the shown item. Without choices the review approves or rejects. A
479
+ * choice's `description` says in plain words what picking it does, with
480
+ * the same placeholders as `detail`.
446
481
  */
447
482
  readonly choices: S.optionalKey<S.$Array<S.Struct<{
448
483
  readonly id: S.String;
449
484
  readonly label: S.String;
485
+ readonly description: S.optionalKey<S.String>;
450
486
  }>>>;
451
487
  }>, S.Struct<{
452
488
  readonly id: S.String;
@@ -595,6 +631,12 @@ export declare const planContentSchema: S.Struct<{
595
631
  readonly id: S.String;
596
632
  readonly kind: S.Literal<"review">;
597
633
  readonly title: S.String;
634
+ /**
635
+ * One plain line under the title saying where the run is, such as
636
+ * `Beat {item} of {items} · try {try} of {tries}`. The placeholders are
637
+ * filled from the loops around the review (see `fillReviewText`).
638
+ */
639
+ readonly detail: S.optionalKey<S.String>;
598
640
  readonly show: S.$Array<S.Struct<{
599
641
  readonly node: S.Union<readonly [S.String, S.Literals<readonly ["$input", "$item"]>]>;
600
642
  readonly port: S.String;
@@ -603,11 +645,14 @@ export declare const planContentSchema: S.Struct<{
603
645
  readonly timeout: S.Number;
604
646
  /**
605
647
  * Named outcomes a reviewer picks from; each is an output port carrying
606
- * the shown item. Without choices the review approves or rejects.
648
+ * the shown item. Without choices the review approves or rejects. A
649
+ * choice's `description` says in plain words what picking it does, with
650
+ * the same placeholders as `detail`.
607
651
  */
608
652
  readonly choices: S.optionalKey<S.$Array<S.Struct<{
609
653
  readonly id: S.String;
610
654
  readonly label: S.String;
655
+ readonly description: S.optionalKey<S.String>;
611
656
  }>>>;
612
657
  }>, S.Struct<{
613
658
  readonly id: S.String;
@@ -723,6 +768,12 @@ export declare const planContentSchema: S.Struct<{
723
768
  readonly id: S.String;
724
769
  readonly kind: S.Literal<"review">;
725
770
  readonly title: S.String;
771
+ /**
772
+ * One plain line under the title saying where the run is, such as
773
+ * `Beat {item} of {items} · try {try} of {tries}`. The placeholders are
774
+ * filled from the loops around the review (see `fillReviewText`).
775
+ */
776
+ readonly detail: S.optionalKey<S.String>;
726
777
  readonly show: S.$Array<S.Struct<{
727
778
  readonly node: S.Union<readonly [S.String, S.Literals<readonly ["$input", "$item"]>]>;
728
779
  readonly port: S.String;
@@ -731,11 +782,14 @@ export declare const planContentSchema: S.Struct<{
731
782
  readonly timeout: S.Number;
732
783
  /**
733
784
  * Named outcomes a reviewer picks from; each is an output port carrying
734
- * the shown item. Without choices the review approves or rejects.
785
+ * the shown item. Without choices the review approves or rejects. A
786
+ * choice's `description` says in plain words what picking it does, with
787
+ * the same placeholders as `detail`.
735
788
  */
736
789
  readonly choices: S.optionalKey<S.$Array<S.Struct<{
737
790
  readonly id: S.String;
738
791
  readonly label: S.String;
792
+ readonly description: S.optionalKey<S.String>;
739
793
  }>>>;
740
794
  }>, S.Struct<{
741
795
  readonly id: S.String;
@@ -876,6 +930,12 @@ export declare const executionPlanSchema: S.Struct<{
876
930
  readonly id: S.String;
877
931
  readonly kind: S.Literal<"review">;
878
932
  readonly title: S.String;
933
+ /**
934
+ * One plain line under the title saying where the run is, such as
935
+ * `Beat {item} of {items} · try {try} of {tries}`. The placeholders are
936
+ * filled from the loops around the review (see `fillReviewText`).
937
+ */
938
+ readonly detail: S.optionalKey<S.String>;
879
939
  readonly show: S.$Array<S.Struct<{
880
940
  readonly node: S.Union<readonly [S.String, S.Literals<readonly ["$input", "$item"]>]>;
881
941
  readonly port: S.String;
@@ -884,11 +944,14 @@ export declare const executionPlanSchema: S.Struct<{
884
944
  readonly timeout: S.Number;
885
945
  /**
886
946
  * Named outcomes a reviewer picks from; each is an output port carrying
887
- * the shown item. Without choices the review approves or rejects.
947
+ * the shown item. Without choices the review approves or rejects. A
948
+ * choice's `description` says in plain words what picking it does, with
949
+ * the same placeholders as `detail`.
888
950
  */
889
951
  readonly choices: S.optionalKey<S.$Array<S.Struct<{
890
952
  readonly id: S.String;
891
953
  readonly label: S.String;
954
+ readonly description: S.optionalKey<S.String>;
892
955
  }>>>;
893
956
  }>, S.Struct<{
894
957
  readonly id: S.String;
@@ -1004,6 +1067,12 @@ export declare const executionPlanSchema: S.Struct<{
1004
1067
  readonly id: S.String;
1005
1068
  readonly kind: S.Literal<"review">;
1006
1069
  readonly title: S.String;
1070
+ /**
1071
+ * One plain line under the title saying where the run is, such as
1072
+ * `Beat {item} of {items} · try {try} of {tries}`. The placeholders are
1073
+ * filled from the loops around the review (see `fillReviewText`).
1074
+ */
1075
+ readonly detail: S.optionalKey<S.String>;
1007
1076
  readonly show: S.$Array<S.Struct<{
1008
1077
  readonly node: S.Union<readonly [S.String, S.Literals<readonly ["$input", "$item"]>]>;
1009
1078
  readonly port: S.String;
@@ -1012,11 +1081,14 @@ export declare const executionPlanSchema: S.Struct<{
1012
1081
  readonly timeout: S.Number;
1013
1082
  /**
1014
1083
  * Named outcomes a reviewer picks from; each is an output port carrying
1015
- * the shown item. Without choices the review approves or rejects.
1084
+ * the shown item. Without choices the review approves or rejects. A
1085
+ * choice's `description` says in plain words what picking it does, with
1086
+ * the same placeholders as `detail`.
1016
1087
  */
1017
1088
  readonly choices: S.optionalKey<S.$Array<S.Struct<{
1018
1089
  readonly id: S.String;
1019
1090
  readonly label: S.String;
1091
+ readonly description: S.optionalKey<S.String>;
1020
1092
  }>>>;
1021
1093
  }>, S.Struct<{
1022
1094
  readonly id: S.String;
package/dist/schema.js CHANGED
@@ -84,14 +84,26 @@ export const nodeSchema = S.Union([
84
84
  id: name,
85
85
  kind: S.Literal("review"),
86
86
  title: boundedString(120),
87
+ /**
88
+ * One plain line under the title saying where the run is, such as
89
+ * `Beat {item} of {items} · try {try} of {tries}`. The placeholders are
90
+ * filled from the loops around the review (see `fillReviewText`).
91
+ */
92
+ detail: S.optionalKey(boundedString(160)),
87
93
  show: array(portRefSchema, 64),
88
94
  reviewers: S.Literals(["runner", "owners"]),
89
95
  timeout: duration,
90
96
  /**
91
97
  * Named outcomes a reviewer picks from; each is an output port carrying
92
- * the shown item. Without choices the review approves or rejects.
98
+ * the shown item. Without choices the review approves or rejects. A
99
+ * choice's `description` says in plain words what picking it does, with
100
+ * the same placeholders as `detail`.
93
101
  */
94
- choices: S.optionalKey(array(S.Struct({ id: name, label: boundedString(80) }), 8)),
102
+ choices: S.optionalKey(array(S.Struct({
103
+ id: name,
104
+ label: boundedString(80),
105
+ description: S.optionalKey(boundedString(160)),
106
+ }), 8)),
95
107
  }),
96
108
  S.Struct({ id: name, kind: S.Literal("condition"), test: predicateSchema }),
97
109
  S.Struct({
package/dist/validate.js CHANGED
@@ -2,6 +2,7 @@ import { escapePointer } from "@cfworker/json-schema";
2
2
  import * as S from "effect/Schema";
3
3
  import * as Result from "effect/Result";
4
4
  import { nextOccurrence } from "@fungi.computer/almanac";
5
+ import { reviewTextPlaceholders, unknownReviewPlaceholders, } from "./review-text.js";
5
6
  import { workflowDefinitionSchema, catalogSchema, TRIGGER_EVENT_PORTS, } from "./schema.js";
6
7
  import { parseProjection, methodInputPorts, methodOutputPorts, portProjection, ports, accepts, assignable, } from "./ports.js";
7
8
  import { nodeRefs, mappingRefs, mappingProjection, predicateRefs, ExpressionSyntaxError, } from "./expressions.js";
@@ -363,6 +364,12 @@ export function analyze(raw, rawCatalog) {
363
364
  case "review": {
364
365
  const shown = node.show[0];
365
366
  const item = shown ? (source(shown) ?? {}) : {};
367
+ for (const text of [
368
+ node.detail ?? "",
369
+ ...(node.choices ?? []).map((choice) => choice.description ?? ""),
370
+ ])
371
+ for (const name of unknownReviewPlaceholders(text))
372
+ issue("schema", `Unknown review placeholder {${name}}; use ${reviewTextPlaceholders.map((known) => `{${known}}`).join(", ")}`, { node: node.id });
366
373
  if (node.choices === undefined) {
367
374
  outs.set(node.id, {
368
375
  approved: item,
@@ -1,3 +1,3 @@
1
- import { z } from "zod";
1
+ import * as z from "zod";
2
2
  /** Identity of a versioned Workflow document, shared by hosts and the Trellis App. */
3
3
  export declare const workflowIdSchema: z.ZodString;
@@ -1,3 +1,3 @@
1
- import { z } from "zod";
1
+ import * as z from "zod";
2
2
  /** Identity of a versioned Workflow document, shared by hosts and the Trellis App. */
3
3
  export const workflowIdSchema = z.string().regex(/^[a-z0-9][a-z0-9-]{0,62}$/);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fungi.computer/braid",
3
- "version": "0.0.1",
3
+ "version": "0.2.0",
4
4
  "author": "Levi <levi@technomancy.dev>",
5
5
  "type": "module",
6
6
  "description": "Pure versioned Workflow graph validation and content-addressed compilation",
@@ -29,16 +29,17 @@
29
29
  ],
30
30
  "dependencies": {
31
31
  "@cfworker/json-schema": "4.1.1",
32
- "@fungi.computer/almanac": "0.0.1",
33
- "@fungi.computer/mycelium": "0.0.2",
32
+ "@fungi.computer/almanac": "0.2.0",
33
+ "@fungi.computer/mycelium": "0.2.0",
34
34
  "effect": "4.0.2",
35
35
  "jsonpointer": "5.0.1",
36
36
  "mustache": "4.2.0",
37
37
  "zod": "4.4.3",
38
- "@fungi.computer/agar": "0.1.0",
39
- "@fungi.computer/watchdog": "0.0.1",
40
- "@fungi.computer/puffball": "0.0.2",
41
- "@fungi.computer/cairn": "0.0.1"
38
+ "@fungi.computer/agar": "0.3.0",
39
+ "@fungi.computer/gills": "0.2.0",
40
+ "@fungi.computer/watchdog": "0.1.0",
41
+ "@fungi.computer/puffball": "0.2.0",
42
+ "@fungi.computer/cairn": "0.1.0"
42
43
  },
43
44
  "devDependencies": {
44
45
  "@cloudflare/workers-types": "5.20260828.1",