@anchrd/intel-contract 0.2.0 → 0.2.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.
@@ -9,9 +9,21 @@ export const ProblemDetails = z.strictObject({
9
9
  instance: z.string().optional(),
10
10
  code: z.string().optional(),
11
11
  });
12
- export const KnowledgeNodeKind = z.enum(["folder", "document", "attachment"]);
12
+ // Who the caller is, as Gate resolved it from the bearer. Deliberately identity only: no
13
+ // capabilities, no token, nothing a surface could mistake for a permission.
14
+ export const SessionUser = z.strictObject({
15
+ id: IntelId,
16
+ email: z.email(),
17
+ name: z.string().min(1).max(240).nullable(),
18
+ });
19
+ // The fourth kind is `table` (#40). It is a kind of node, not a kind of thing: it hangs in the same
20
+ // folder tree, inherits the same folder grants, carries the same immutable versions and the same
21
+ // R2 body as a document (ADR-0004 §1). Only its media type and the one operation below differ.
22
+ export const KnowledgeNodeKind = z.enum(["folder", "document", "attachment", "table"]);
13
23
  export const ContextPolicy = z.enum(["pinned", "relevant", "explicit"]);
14
- export const ResourceRole = z.enum(["viewer", "commenter", "editor", "manager"]);
24
+ // One verb per grant, granted independently (ADR-0004 §2). Not a ladder: seeing a process must be
25
+ // separable from being allowed to start it, and `execute` is meaningful only where a flow can live.
26
+ export const ResourceVerb = z.enum(["read", "write", "execute", "share"]);
15
27
  export const SharePrincipal = z.discriminatedUnion("type", [
16
28
  z.strictObject({ type: z.literal("user"), id: IntelId }),
17
29
  z.strictObject({ type: z.literal("email"), email: z.email() }),
@@ -101,32 +113,94 @@ export const KnowledgeAttachment = z.strictObject({
101
113
  version: KnowledgeVersion,
102
114
  resourceUri: z.string().regex(/^intel:\/\/knowledge\/[^/]+\/attachment$/),
103
115
  });
116
+ // A table is CSV, and CSV is the whole format: it is what is stored, what is downloaded and what a
117
+ // machine reads. There is no second representation to keep in step with it (#40).
118
+ export const TableMediaType = "text/csv";
119
+ // A column name is the contract between the table and everyone who appends to it, so it is trimmed,
120
+ // non-empty and bounded like a title. Cells are not: a cell is text, and text is what CSV carries.
121
+ export const TableColumn = z.string().trim().min(1).max(120);
122
+ export const TableCell = z.string().max(4_000);
123
+ export const TableRow = z.array(TableCell).min(1).max(64);
124
+ // Writing the header, once. The columns are the contract (#40's comment), which is why this refuses
125
+ // on a table that already has one: changing the header would silently reinterpret every row that
126
+ // was appended under the old one.
127
+ export const DefineKnowledgeTableInput = z.strictObject({
128
+ nodeId: IntelId,
129
+ columns: z
130
+ .array(TableColumn)
131
+ .min(1)
132
+ .max(64)
133
+ .refine((columns) => new Set(columns.map((column) => column.toLowerCase())).size === columns.length, { error: "Column names must be distinct" }),
134
+ idempotencyKey: z.string().min(8).max(200),
135
+ });
136
+ // ⚠️ No `baseVersionId`, and that absence is the feature. A document replaces its content and needs
137
+ // to know which content it replaces; an append adds to the end and cannot collide with a second
138
+ // append, so demanding a base version would invent a conflict that does not exist and force the
139
+ // caller to read the whole table first — the exact cost #40 exists to remove.
140
+ export const AppendKnowledgeTableRowsInput = z.strictObject({
141
+ nodeId: IntelId,
142
+ rows: z.array(TableRow).min(1).max(1_000),
143
+ idempotencyKey: z.string().min(8).max(200),
144
+ });
145
+ export const GetKnowledgeTableInput = z.strictObject({ nodeId: IntelId });
146
+ // The table as a grid rather than as text: the server owns the one CSV reader, so no surface has to
147
+ // grow a second one that would disagree with it about quoting.
148
+ export const KnowledgeTable = z.strictObject({
149
+ node: KnowledgeNode,
150
+ columns: z.array(z.string()),
151
+ rows: z.array(z.array(z.string())),
152
+ // The newest append, or `null` while the table has no header yet.
153
+ versionId: IntelId.nullable(),
154
+ });
155
+ export const AppendKnowledgeTableRowsResult = z.strictObject({
156
+ node: KnowledgeNode,
157
+ version: KnowledgeVersion,
158
+ appended: z.number().int().positive(),
159
+ });
160
+ // ⚠️ Kept for what is already stored, not for what is written. Relations were picked in a dialog
161
+ // until #41; a link is now made where it is meant — in the text — and every link written from now
162
+ // on is a `references`. Rewriting the old rows would destroy a distinction somebody chose on
163
+ // purpose, and dropping the column would destroy it with them, so both stay readable.
104
164
  export const KnowledgeLinkRelation = z.enum(["references", "related", "depends_on", "implements"]);
165
+ // Where the link came from. `text` links are derived from a document's content and are rewritten
166
+ // whenever it is saved; `manual` links were made in the dialog #41 removed and are now history.
167
+ //
168
+ // ⚠️ This is provenance, never a second sort of relationship. Nothing offers the reader a choice
169
+ // between them, and nothing may start writing `manual` again — that would be the two ways of saying
170
+ // one thing that #41 exists to end. It exists so that saving a document cannot delete a link
171
+ // somebody made before there was another way to make one.
172
+ export const KnowledgeLinkOrigin = z.enum(["text", "manual"]);
105
173
  export const KnowledgeLink = z.strictObject({
106
174
  id: IntelId,
107
175
  sourceNodeId: IntelId,
108
176
  targetNodeId: IntelId,
109
177
  relation: KnowledgeLinkRelation,
178
+ origin: KnowledgeLinkOrigin,
110
179
  label: z.string().trim().min(1).max(120).nullable(),
111
180
  createdBy: IntelId,
112
181
  createdAt: IsoDateTime,
113
182
  });
114
183
  export const KnowledgeLinkList = z.strictObject({ items: z.array(KnowledgeLink) });
115
- export const CreateKnowledgeLinkInput = z
116
- .strictObject({
117
- sourceNodeId: IntelId,
118
- targetNodeId: IntelId,
119
- relation: KnowledgeLinkRelation,
120
- label: z.string().trim().min(1).max(120).nullable().default(null),
121
- idempotencyKey: z.string().min(8).max(200),
122
- })
123
- .refine((input) => input.sourceNodeId !== input.targetNodeId, {
124
- message: "Knowledge cannot link to itself",
184
+ // The inline element a document link is, inside a BlockNote document (#41).
185
+ //
186
+ // ⚠️ The ID and nothing else. No title and no path travel with it: a stored title would go stale
187
+ // the moment the target is renamed, a stored path the moment it is moved — and either one would
188
+ // put a name the reader may not see into a document they may.
189
+ export const DocumentLinkInlineType = "documentLink";
190
+ export const ResolveKnowledgeLinksInput = z.strictObject({
191
+ nodeIds: z.array(IntelId).min(1).max(200),
192
+ });
193
+ export const ResolvedKnowledgeLink = z.strictObject({
194
+ nodeId: IntelId,
195
+ title: z.string().min(1).max(240),
125
196
  });
126
- export const DeleteKnowledgeLinkInput = z.strictObject({
127
- sourceNodeId: IntelId,
128
- linkId: IntelId,
129
- idempotencyKey: z.string().min(8).max(200),
197
+ // ⚠️ Only what the asking reader may see is in here, and an unreachable target is simply absent —
198
+ // never a row with an empty title, never a count, never a "restricted" marker. The list is what one
199
+ // side of a document link is drawn from, and an entry that says "something is here" is exactly the
200
+ // leak this schema has to make impossible to write by accident. Deleted and unreadable therefore
201
+ // look identical from the outside, which is the point.
202
+ export const ResolveKnowledgeLinksResult = z.strictObject({
203
+ items: z.array(ResolvedKnowledgeLink),
130
204
  });
131
205
  export const KnowledgeGraphInput = z.strictObject({
132
206
  limit: z.number().int().min(1).max(500).default(250),
@@ -146,7 +220,7 @@ export const ResourceGrant = z.strictObject({
146
220
  id: IntelId,
147
221
  resourceId: IntelId,
148
222
  principal: SharePrincipal,
149
- role: ResourceRole,
223
+ verb: ResourceVerb,
150
224
  expiresAt: IsoDateTime.nullable(),
151
225
  createdBy: IntelId,
152
226
  createdAt: IsoDateTime,
@@ -154,7 +228,7 @@ export const ResourceGrant = z.strictObject({
154
228
  export const ShareKnowledgeInput = z.strictObject({
155
229
  resourceId: IntelId,
156
230
  principal: SharePrincipal,
157
- role: ResourceRole,
231
+ verb: ResourceVerb,
158
232
  expiresAt: IsoDateTime.nullable().default(null),
159
233
  idempotencyKey: z.string().min(8).max(200),
160
234
  });
@@ -163,7 +237,30 @@ export const RevokeKnowledgeGrantInput = z.strictObject({
163
237
  grantId: IntelId,
164
238
  idempotencyKey: z.string().min(8).max(200),
165
239
  });
166
- export const ResourceGrantList = z.strictObject({ items: z.array(ResourceGrant) });
240
+ // What a grant does not cover, reported to whoever just made it. A flow in the shared folder may
241
+ // read a document outside it, and the run is re-authorized against the person running it — so the
242
+ // grant can be complete and the flow still stop for them (ADR-0004 §4).
243
+ //
244
+ // ⚠️ `titles` holds only the documents the sharer may see; everything else is in `hidden` as a
245
+ // number. A warning must not become a way of reading titles out of the tree.
246
+ export const UnreadableKnowledge = z.strictObject({
247
+ titles: z.array(z.string().min(1).max(240)),
248
+ hidden: z.number().int().nonnegative(),
249
+ });
250
+ // The grant is in the answer, so the warning cannot be mistaken for a refusal: it is written first
251
+ // and described afterwards. Blocking would force everyone who uses a central policy document to
252
+ // duplicate it, which is the opposite of what one tree is for (ADR-0004 §4).
253
+ export const ShareKnowledgeResult = z.strictObject({
254
+ grant: ResourceGrant,
255
+ unreadable: UnreadableKnowledge,
256
+ });
257
+ // `applicableVerbs` travels with the list because the answer is the business layer's, not the
258
+ // screen's: a document has nothing to execute, so `execute` is not offered on one (ADR-0004 §2).
259
+ export const ResourceGrantList = z.strictObject({
260
+ resourceId: IntelId,
261
+ applicableVerbs: z.array(ResourceVerb).min(1),
262
+ items: z.array(ResourceGrant),
263
+ });
167
264
  export const SearchKnowledgeInput = z.strictObject({
168
265
  query: z.string().trim().min(1).max(500),
169
266
  limit: z.number().int().min(1).max(50).default(10),
@@ -183,7 +280,6 @@ export const SearchKnowledgeResult = z.strictObject({
183
280
  });
184
281
  export const ReindexKnowledgeResult = z.strictObject({ queued: z.number().int().nonnegative() });
185
282
  export const RevokeGrantResult = z.strictObject({ revoked: z.boolean() });
186
- export const DeleteKnowledgeLinkResult = z.strictObject({ deleted: z.boolean() });
187
283
  function isPrivateIpv4(hostname) {
188
284
  const parts = hostname.split(".").map(Number);
189
285
  if (parts.length !== 4 ||
@@ -265,21 +361,42 @@ export const ToolTestResult = z.strictObject({
265
361
  });
266
362
  export const FlowNodeId = z.string().regex(/^[A-Za-z0-9][A-Za-z0-9_-]{0,99}$/);
267
363
  export const FlowPosition = z.strictObject({ x: z.number().finite(), y: z.number().finite() });
364
+ // A node has a title and nothing else to write in prose (#39). A second free text beside it was
365
+ // kept half up to date on both sides, and for an instruction the same sentence already belongs in
366
+ // the instruction itself.
268
367
  const FlowNodeBase = {
269
368
  id: FlowNodeId,
270
369
  position: FlowPosition,
271
370
  label: z.string().trim().min(1).max(160),
272
- description: z.string().trim().max(2_000).nullable().default(null),
273
371
  };
372
+ // Which version of the callee a sub-flow call takes (ADR-0004 §5). Three states rather than an
373
+ // optional identifier, because the difference between them is a decision and has to be readable:
374
+ //
375
+ // - `latest` the draft default. Nobody should have to version things while building, and
376
+ // publishing turns this into `pinned` — visibly, before the author publishes.
377
+ // - `follows` "always latest", chosen on purpose. The call rides along with the callee, so a
378
+ // change to the building block changes this flow too. Publishing leaves it alone.
379
+ // - `pinned` one immutable version, whatever is published elsewhere.
380
+ //
381
+ // ⚠️ Without the freeze a change to a building block would silently change the behavior of every
382
+ // published flow using it, which contradicts the immutable versions and pinned schema fingerprints
383
+ // Intel otherwise guarantees. That is why `latest` cannot survive publishing.
384
+ export const SubflowVersionMode = z.enum(["latest", "follows", "pinned"]);
385
+ export const SubflowVersion = z.discriminatedUnion("mode", [
386
+ z.strictObject({ mode: z.literal("latest") }),
387
+ z.strictObject({ mode: z.literal("follows") }),
388
+ z.strictObject({ mode: z.literal("pinned"), versionId: IntelId }),
389
+ ]);
274
390
  export const FlowNode = z.discriminatedUnion("kind", [
275
391
  z.strictObject({
276
392
  ...FlowNodeBase,
277
393
  kind: z.literal("trigger"),
278
- configuration: z.discriminatedUnion("mode", [
279
- z.strictObject({ mode: z.literal("manual") }),
280
- z.strictObject({ mode: z.literal("webhook"), event: z.string().min(1).max(120) }),
281
- z.strictObject({ mode: z.literal("schedule"), cron: z.string().min(5).max(120) }),
282
- ]),
394
+ // ⚠️ `manual` is the only mode there is. `webhook` and `schedule` stood here and fired nothing:
395
+ // a flow is carried out by an external agent that brings its own schedule, which is what the
396
+ // code does rather than what it intends — `step()` hands back the current node, `completeStep`
397
+ // takes the result from outside, and the Cloudflare workflow waits rather than drives. #32 was
398
+ // closed on that basis, and a mode nothing triggers is a promise nobody keeps.
399
+ configuration: z.strictObject({ mode: z.literal("manual") }),
283
400
  }),
284
401
  z.strictObject({
285
402
  ...FlowNodeBase,
@@ -324,16 +441,37 @@ export const FlowNode = z.discriminatedUnion("kind", [
324
441
  timeout: z.string().regex(/^\d+\s+(minute|minutes|hour|hours|day|days)$/),
325
442
  }),
326
443
  }),
444
+ // The eighth kind: one flow calls another (ADR-0004 §3). A schema addition, not hidden behavior in
445
+ // a generic code node — which flow is called has to be readable from the graph, or neither the
446
+ // publish-time call rule nor the sidebar could see it.
447
+ z.strictObject({
448
+ ...FlowNodeBase,
449
+ kind: z.literal("subflow"),
450
+ // What the called flow is given travels through `StartFlowRunInput.input` — the schema every run
451
+ // already uses. A second, static input here would be a promise the execution does not keep.
452
+ configuration: z.strictObject({
453
+ flowId: IntelId,
454
+ version: SubflowVersion.default({ mode: "latest" }),
455
+ }),
456
+ }),
327
457
  z.strictObject({
328
458
  ...FlowNodeBase,
329
459
  kind: z.literal("output"),
330
460
  configuration: z.strictObject({ template: z.string().max(50_000).default("") }),
331
461
  }),
332
462
  ]);
463
+ // The two things an edge can mean (#37). `flow` is the order of work — "and then". `context` is what
464
+ // a step works with: a document consulted in exactly this step, an approval obtained in exactly this
465
+ // step, or, at the output, the table a result is written to.
466
+ //
467
+ // ⚠️ `flow` is the default, and that is the whole of the migration: every edge stored before this
468
+ // existed parses into the meaning it already had. Nothing about saved graphs has to be rewritten.
469
+ export const FlowEdgeKind = z.enum(["flow", "context"]);
333
470
  export const FlowEdge = z.strictObject({
334
471
  id: FlowNodeId,
335
472
  source: FlowNodeId,
336
473
  target: FlowNodeId,
474
+ kind: FlowEdgeKind.default("flow"),
337
475
  label: z.string().trim().min(1).max(120).nullable().default(null),
338
476
  sourceHandle: z.string().trim().min(1).max(120).nullable().default(null),
339
477
  });
@@ -343,6 +481,10 @@ export const FlowGraph = z.strictObject({
343
481
  });
344
482
  export const Flow = z.strictObject({
345
483
  id: IntelId,
484
+ // The one thing a Flow shares with a document: its place in the Knowledge folder tree (ADR-0004).
485
+ // Everything else stays apart — versions, R2 body and Vectorize belong to the document, the graph,
486
+ // runs and approvals to the flow. `null` is the root of that same tree.
487
+ parentId: IntelId.nullable(),
346
488
  title: z.string().min(1).max(240),
347
489
  description: z.string().max(2_000).nullable(),
348
490
  ownerId: IntelId,
@@ -365,11 +507,50 @@ export const FlowDocument = z.strictObject({
365
507
  version: FlowVersion.nullable(),
366
508
  });
367
509
  export const FlowList = z.strictObject({ items: z.array(Flow) });
510
+ export const FlowKnowledgeReference = z.strictObject({
511
+ id: IntelId,
512
+ title: z.string().min(1).max(240),
513
+ });
514
+ // What a flow touches: the documents its Knowledge steps name and the tools its Tool steps call,
515
+ // read straight out of the graph. Deliberately not a conflict report — there is no arithmetic here
516
+ // and nothing that can go stale, because the graph is the answer. Whether a given person may reach
517
+ // any of it is decided where it can be decided honestly: when the folder is shared, and at runtime
518
+ // (ADR-0004 §4). For tools it can only ever be the latter, because the catalog is a live query with
519
+ // the requesting user's own token (ADR-0003).
520
+ //
521
+ // ⚠️ `knowledge` names only what the asking user may see. The rest is `hiddenKnowledge`, a count.
522
+ export const FlowRequirements = z.strictObject({
523
+ flowId: IntelId,
524
+ versionId: IntelId.nullable(),
525
+ knowledge: z.array(FlowKnowledgeReference),
526
+ hiddenKnowledge: z.number().int().nonnegative(),
527
+ tools: z.array(ToolName),
528
+ });
368
529
  export const CreateFlowInput = z.strictObject({
530
+ parentId: IntelId.nullable().default(null),
369
531
  title: z.string().trim().min(1).max(240),
370
532
  description: z.string().trim().max(2_000).nullable().default(null),
371
533
  idempotencyKey: z.string().min(8).max(200),
372
534
  });
535
+ // Renaming and moving a flow. Both are organization and nothing else: they touch no version, no
536
+ // published graph and no run, because organization has to stay free of consequence or nobody dares
537
+ // to reorganize (ADR-0004).
538
+ export const UpdateFlowInput = z
539
+ .strictObject({
540
+ flowId: IntelId,
541
+ baseUpdatedAt: IsoDateTime,
542
+ title: z.string().trim().min(1).max(240).optional(),
543
+ description: z.string().trim().max(2_000).nullable().optional(),
544
+ parentId: IntelId.nullable().optional(),
545
+ idempotencyKey: z.string().min(8).max(200),
546
+ })
547
+ .refine((input) => input.title !== undefined || input.description !== undefined || input.parentId !== undefined, { error: "At least one change is required" });
548
+ // Three answers, not two: an absent `parentId` lists every visible flow (the search dialog asks
549
+ // that), `null` lists the root of the shared tree and an ID lists one folder (the sidebar tree asks
550
+ // per level, which is what keeps the tree off the N+1 it used to load with).
551
+ export const ListFlowsInput = z.strictObject({
552
+ parentId: IntelId.nullable().optional(),
553
+ });
373
554
  export const GetFlowInput = z.strictObject({ flowId: IntelId });
374
555
  export const SaveFlowVersionInput = z.strictObject({
375
556
  flowId: IntelId,
@@ -382,18 +563,73 @@ export const PublishFlowInput = z.strictObject({
382
563
  versionId: IntelId,
383
564
  idempotencyKey: z.string().min(8).max(200),
384
565
  });
385
- export const ListFlowGrantsInput = z.strictObject({ resourceId: IntelId });
386
- export const ShareFlowInput = z.strictObject({
387
- resourceId: IntelId,
388
- principal: SharePrincipal,
389
- role: ResourceRole,
390
- expiresAt: IsoDateTime.nullable().default(null),
391
- idempotencyKey: z.string().min(8).max(200),
392
- });
393
- export const RevokeFlowGrantInput = z.strictObject({
394
- resourceId: IntelId,
395
- grantId: IntelId,
396
- idempotencyKey: z.string().min(8).max(200),
566
+ export const PreviewFlowPublishInput = z.strictObject({ flowId: IntelId, versionId: IntelId });
567
+ // One sub-flow call as publishing will leave it (ADR-0004 §5). `freezes` is the whole point of the
568
+ // preview: it marks the calls whose `latest` publishing turns into `versionId`, so the author reads
569
+ // the decision before making it rather than after.
570
+ //
571
+ // ⚠️ Only callees the asking actor may reach are listed at all. A call whose callee they cannot see
572
+ // is left out rather than named, because a title is the thing an unreachable flow must not hand out.
573
+ export const FlowPublishCall = z.strictObject({
574
+ nodeId: FlowNodeId,
575
+ nodeLabel: z.string().min(1).max(160),
576
+ calleeId: IntelId,
577
+ calleeTitle: z.string().min(1).max(240),
578
+ mode: SubflowVersionMode,
579
+ // The version this call will take once published. `null` when it follows the callee, which is the
580
+ // one case where the answer is only known at run time.
581
+ versionId: IntelId.nullable(),
582
+ versionSequence: z.number().int().positive().nullable(),
583
+ freezes: z.boolean(),
584
+ // The callee has a published version to be called at all. `false` is what publishing will refuse.
585
+ available: z.boolean(),
586
+ });
587
+ export const FlowPublishPreview = z.strictObject({
588
+ flowId: IntelId,
589
+ versionId: IntelId,
590
+ calls: z.array(FlowPublishCall),
591
+ });
592
+ // A flow has no share schema of its own. A grant sits on the folder a flow is filed in and inherits
593
+ // down from there (ADR-0004 §2); a narrower grant beside it would destroy the subtree guarantee
594
+ // section 3 rests on, so per-flow grants were removed rather than deprecated.
595
+ // What accesses what, for one level of the shared tree (#19). A folder answers it for its contents,
596
+ // a single flow for itself. Documents and flows are two kinds of thing that share one tree
597
+ // (ADR-0004 §1), so the graph carries both and says which of them it is.
598
+ export const RelationNodeKind = z.enum(["folder", "document", "attachment", "table", "flow"]);
599
+ export const RelationNode = z.strictObject({
600
+ id: IntelId,
601
+ kind: RelationNodeKind,
602
+ title: z.string().min(1).max(240),
603
+ // Inside the level being shown, rather than something it reaches out to. A flow reading a policy
604
+ // document from another folder pulls that document in, and the difference should be legible.
605
+ inScope: z.boolean(),
606
+ });
607
+ export const RelationEdge = z.strictObject({
608
+ id: z.string().min(1).max(400),
609
+ source: IntelId,
610
+ target: IntelId,
611
+ relation: z.enum(["reads", "calls"]),
612
+ });
613
+ export const RelationGraphScope = z.discriminatedUnion("of", [
614
+ z.strictObject({ of: z.literal("folder"), folderId: IntelId.nullable() }),
615
+ z.strictObject({ of: z.literal("flow"), flowId: IntelId }),
616
+ ]);
617
+ export const RelationGraphInput = z.strictObject({
618
+ scope: RelationGraphScope,
619
+ // How much is drawn before the answer is summarized instead. A big folder has to stay usable, and
620
+ // the cut-off is reported rather than swallowed.
621
+ limit: z.number().int().min(1).max(300).default(60),
622
+ });
623
+ // ⚠️ Only what the asking user may see is in here. A node they may not reach is absent, not greyed
624
+ // out and not counted: an edge to a placeholder would already tell them the thing exists, which is
625
+ // the leak this schema has to make impossible to write by accident. `omitted` is about the size
626
+ // limit alone, never about permissions.
627
+ export const RelationGraph = z.strictObject({
628
+ scope: RelationGraphScope,
629
+ nodes: z.array(RelationNode),
630
+ edges: z.array(RelationEdge),
631
+ omitted: z.number().int().nonnegative(),
632
+ limit: z.number().int().positive(),
397
633
  });
398
634
  export const FlowRunStatus = z.enum([
399
635
  "queued",
@@ -413,14 +649,36 @@ export const FlowRun = z.strictObject({
413
649
  output: z.unknown().nullable(),
414
650
  error: z.string().max(2_000).nullable(),
415
651
  initiatedBy: IntelId,
652
+ // The subflow node this run was called from, and the run that node belongs to. A called run is a
653
+ // run of its own: it has its own version, its own steps and its own authorization, and only these
654
+ // two fields say where its result goes back to.
655
+ parentRunId: IntelId.nullable().default(null),
656
+ parentNodeId: FlowNodeId.nullable().default(null),
416
657
  createdAt: IsoDateTime,
417
658
  updatedAt: IsoDateTime,
418
659
  completedAt: IsoDateTime.nullable(),
419
660
  });
420
- export const FlowRunStep = z.strictObject({ run: FlowRun, node: FlowNode.nullable() });
661
+ // Which step of which flow is running, outermost caller first. Readable rather than reconstructed
662
+ // from `parentRunId` by whoever is looking (#17).
663
+ export const FlowRunTrailEntry = z.strictObject({
664
+ runId: IntelId,
665
+ flowId: IntelId,
666
+ flowTitle: z.string().min(1).max(240),
667
+ nodeId: FlowNodeId.nullable(),
668
+ nodeLabel: z.string().max(160).nullable(),
669
+ });
670
+ export const FlowRunStep = z.strictObject({
671
+ run: FlowRun,
672
+ node: FlowNode.nullable(),
673
+ trail: z.array(FlowRunTrailEntry).default([]),
674
+ });
421
675
  export const StartFlowRunInput = z.strictObject({
422
676
  flowId: IntelId,
423
677
  input: z.record(z.string(), z.unknown()).default({}),
678
+ // Present when this run is the call a subflow node makes. It names a place, never a permission:
679
+ // the callee's `execute` is asked of the user exactly as it is for a run they start themselves,
680
+ // and the parent run must be the caller's own and standing on that very node.
681
+ parent: z.strictObject({ runId: IntelId, nodeId: FlowNodeId }).nullable().default(null),
424
682
  idempotencyKey: z.string().min(8).max(200),
425
683
  });
426
684
  export const GetFlowRunInput = z.strictObject({ runId: IntelId });
@@ -433,3 +691,81 @@ export const CompleteFlowRunStepInput = z.strictObject({
433
691
  error: z.string().max(2_000).nullable().default(null),
434
692
  idempotencyKey: z.string().min(8).max(200),
435
693
  });
694
+ // Why a run started. Derived when it is read and deliberately not a column: `subflow` when the run
695
+ // is the call another run made, otherwise the mode of the trigger node in the immutable version the
696
+ // run took. A stored copy would be a second answer that could disagree with the graph that ran.
697
+ //
698
+ // ⚠️ `webhook` and `schedule` stood here until #39 took them out of the trigger node. Being derived
699
+ // rather than stored is exactly what makes that safe: no run carries a trigger of its own, so once
700
+ // the 0005 migration has rewritten every stored trigger to `manual`, there is nowhere left for the
701
+ // old values to come from. Had this been a column, the enum would have had to keep reading them or
702
+ // every old run would have failed to parse the moment somebody opened the list.
703
+ export const FlowRunTrigger = z.enum(["manual", "subflow"]);
704
+ // Which step ended a run, and why, in the words the failure already used (#20).
705
+ //
706
+ // ⚠️ A call that failed carries its reason in the *called* run, and that run is a run of its own
707
+ // with its own authorization. `calledRunId` is therefore filled only when the asking user may see
708
+ // that run through the very rule every other run answer uses; otherwise the failure is named by the
709
+ // calling step alone — the caller's own label — and `detail` says no more than that it did not
710
+ // finish. Naming a callee's step or document here would be the leak #17, #19 and #20 each closed.
711
+ export const FlowRunFailure = z.strictObject({
712
+ nodeId: FlowNodeId,
713
+ nodeLabel: z.string().max(160),
714
+ detail: z.string().max(2_000),
715
+ calledRunId: IntelId.nullable(),
716
+ });
717
+ // One run as a list shows it: what it did, never what it produced.
718
+ //
719
+ // ⚠️ Neither `input` nor `output` is in here, on purpose. A run reaches its Knowledge and its tools
720
+ // with the rights of whoever started it, so its result is a way to content the next reader of this
721
+ // list may have no claim to. Whoever wants a result asks for the run itself, where the same rule
722
+ // decides again.
723
+ export const FlowRunSummary = z.strictObject({
724
+ id: IntelId,
725
+ flowId: IntelId,
726
+ // The version the run took. Together with the run's stored input it is what a later "run this
727
+ // again with the old data" would need; replaying is a separate ticket, this only keeps it possible.
728
+ versionId: IntelId,
729
+ status: FlowRunStatus,
730
+ trigger: FlowRunTrigger,
731
+ startedAt: IsoDateTime,
732
+ completedAt: IsoDateTime.nullable(),
733
+ durationMs: z.number().int().nonnegative().nullable(),
734
+ initiatedBy: IntelId,
735
+ parentRunId: IntelId.nullable(),
736
+ failure: FlowRunFailure.nullable(),
737
+ });
738
+ // One filter and nothing else: "only the failed ones" is the question asked in almost every case,
739
+ // and every further facet is a report rather than a search for a fault.
740
+ export const ListFlowRunsInput = z.strictObject({
741
+ flowId: IntelId,
742
+ failedOnly: z.boolean().default(false),
743
+ limit: z.number().int().min(1).max(50).default(20),
744
+ // The `nextCursor` of the previous page. Keyset rather than an offset, because runs arrive while
745
+ // someone reads and an offset would skip or repeat rows exactly when a flow is busy.
746
+ cursor: z.string().min(1).max(400).nullable().default(null),
747
+ });
748
+ export const FlowRunList = z.strictObject({
749
+ items: z.array(FlowRunSummary),
750
+ nextCursor: z.string().max(400).nullable(),
751
+ });
752
+ // One completed step of one run. `detail` is the step's own error text; an output is absent for the
753
+ // same reason it is absent from the summary.
754
+ export const FlowRunStepRecord = z.strictObject({
755
+ nodeId: FlowNodeId,
756
+ nodeLabel: z.string().max(160),
757
+ outcome: z.enum(["completed", "failed"]),
758
+ branch: z.string().max(120).nullable(),
759
+ detail: z.string().max(2_000).nullable(),
760
+ calledRunId: IntelId.nullable(),
761
+ completedAt: IsoDateTime,
762
+ });
763
+ // What one run did, step by step, oldest first, with the call chain it belongs to (#17). The trail
764
+ // is what makes a nested run readable: which step of which flow this run is.
765
+ export const FlowRunHistory = z.strictObject({
766
+ runId: IntelId,
767
+ flowId: IntelId,
768
+ status: FlowRunStatus,
769
+ steps: z.array(FlowRunStepRecord),
770
+ trail: z.array(FlowRunTrailEntry),
771
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anchrd/intel-contract",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {