@llm4ts/flow 0.11.0 → 0.13.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.
@@ -1,15 +1,22 @@
1
- import * as Duration from "effect/Duration";
2
1
  import * as Effect from "effect/Effect";
3
- import * as Redacted from "effect/Redacted";
4
2
  import * as Schema from "effect/Schema";
5
3
  import { Capabilities } from "@llm4ts/core/Capability";
6
4
  import { ProcessError } from "./FlowError.js";
7
5
  import { guarded } from "./CapabilityGuard.js";
8
- export class AdoRequest extends Schema.Class("AdoRequest")({
9
- method: Schema.String,
10
- url: Schema.String,
11
- body: Schema.optionalKey(Schema.String),
12
- contentType: Schema.String.pipe(Schema.withConstructorDefault(Effect.succeed("application/json")))
6
+ // Azure DevOps through the `az` CLI (ADR 0011), the sibling of GitHubTool's
7
+ // `gh` protocol: pure args builders, schema-decoded `--output json`, and
8
+ // capability guards around a ProcessExecutor. Credentials belong to the CLI
9
+ // (`az devops login`, or AZURE_DEVOPS_EXT_PAT in the process environment)
10
+ // this module never reads, holds, or forwards a PAT, so no secret can reach
11
+ // argv, a log line, or a persisted plan.
12
+ export class AdoConfig extends Schema.Class("AdoConfig")({
13
+ orgUrl: Schema.String,
14
+ project: Schema.String,
15
+ repository: Schema.String,
16
+ // Only `az devops invoke` (the comments REST resource, which has no
17
+ // first-class `az boards` verb) needs an explicit API version; every
18
+ // other call is a versioned CLI command.
19
+ apiVersion: Schema.String.pipe(Schema.withConstructorDefault(Effect.succeed("7.1-preview")))
13
20
  }) {
14
21
  }
15
22
  export class WorkItem extends Schema.Class("WorkItem")({
@@ -18,7 +25,16 @@ export class WorkItem extends Schema.Class("WorkItem")({
18
25
  description: Schema.String,
19
26
  acceptanceCriteria: Schema.String,
20
27
  state: Schema.String,
21
- tags: Schema.Array(Schema.String)
28
+ tags: Schema.Array(Schema.String),
29
+ createdBy: Schema.String,
30
+ changedDate: Schema.String
31
+ }) {
32
+ }
33
+ export class WorkItemComment extends Schema.Class("WorkItemComment")({
34
+ id: Schema.Int,
35
+ author: Schema.String,
36
+ text: Schema.String,
37
+ createdDate: Schema.String
22
38
  }) {
23
39
  }
24
40
  export class AdoPullRequest extends Schema.Class("AdoPullRequest")({
@@ -28,136 +44,525 @@ export class AdoPullRequest extends Schema.Class("AdoPullRequest")({
28
44
  webUrl: Schema.String
29
45
  }) {
30
46
  }
31
- export class AdoConfig extends Schema.Class("AdoConfig")({
32
- orgUrl: Schema.String,
33
- project: Schema.String,
34
- repository: Schema.String,
35
- pat: Schema.Redacted(Schema.String, {
36
- disallowJsonEncode: true
37
- }),
38
- apiVersion: Schema.String.pipe(Schema.withConstructorDefault(Effect.succeed("7.1")))
47
+ // Azure DevOps branch policies are the analogue of GitHub's check rollup.
48
+ // There is no policy status for a timed-out run, so the outcome set is the
49
+ // GitHub one minus "TimedOut".
50
+ export const PolicyOutcome = Schema.Literals(["Success", "Failure", "Pending"]);
51
+ export const WorkItemState = Schema.Literals(["open", "closed", "all"]);
52
+ // `refs/heads/x` and `x` both name a branch across the Azure DevOps
53
+ // surface; the CLI wants the short form, so every ref is normalized once
54
+ // on the way into argv.
55
+ export const branchName = (ref) => ref.trim().replace(/^refs\/heads\//, "");
56
+ const org = (config) => [
57
+ "--org",
58
+ config.orgUrl,
59
+ // Without this the CLI probes the working directory's git remote and
60
+ // silently retargets another organization; a library must not guess.
61
+ "--detect",
62
+ "false"
63
+ ];
64
+ const json = ["--output", "json"];
65
+ // ---------------------------------------------------------------------------
66
+ // Work item argv
67
+ // ---------------------------------------------------------------------------
68
+ export const workItemShowArgs = (config, id, expand) => [
69
+ "boards",
70
+ "work-item",
71
+ "show",
72
+ "--id",
73
+ String(id),
74
+ ...(expand === undefined ? [] : ["--expand", expand]),
75
+ ...org(config),
76
+ ...json
77
+ ];
78
+ export const fieldArgs = (fields) => {
79
+ const pairs = Object.entries(fields).map(([name, value]) => `${name}=${value}`);
80
+ return pairs.length === 0 ? [] : ["--fields", ...pairs];
81
+ };
82
+ export const workItemUpdateArgs = (config, id, fields) => [
83
+ "boards",
84
+ "work-item",
85
+ "update",
86
+ "--id",
87
+ String(id),
88
+ ...fieldArgs(fields),
89
+ ...org(config),
90
+ ...json
91
+ ];
92
+ export const workItemCommentArgs = (config, id, text) => [
93
+ "boards",
94
+ "work-item",
95
+ "update",
96
+ "--id",
97
+ String(id),
98
+ "--discussion",
99
+ text,
100
+ ...org(config),
101
+ ...json
102
+ ];
103
+ export const workItemCreateArgs = (config, workItemType, title, description, tags) => [
104
+ "boards",
105
+ "work-item",
106
+ "create",
107
+ "--title",
108
+ title,
109
+ "--type",
110
+ workItemType,
111
+ "--description",
112
+ description,
113
+ "--project",
114
+ config.project,
115
+ ...fieldArgs(tags.length === 0 ? {} : { "System.Tags": tags.join("; ") }),
116
+ ...org(config),
117
+ ...json
118
+ ];
119
+ export const commentsArgs = (config, id) => [
120
+ "devops",
121
+ "invoke",
122
+ "--area",
123
+ "wit",
124
+ "--resource",
125
+ "comments",
126
+ "--route-parameters",
127
+ `project=${config.project}`,
128
+ `workItemId=${String(id)}`,
129
+ "--api-version",
130
+ config.apiVersion,
131
+ ...org(config),
132
+ ...json
133
+ ];
134
+ // ---------------------------------------------------------------------------
135
+ // WIQL
136
+ // ---------------------------------------------------------------------------
137
+ // WIQL string literals are single-quoted; a quote inside a value is escaped
138
+ // by doubling it. Every caller-supplied value goes through here so a tag
139
+ // like "won't fix" cannot terminate the literal and rewrite the query.
140
+ export const quoteWiql = (value) => `'${value.replace(/'/g, "''")}'`;
141
+ export const workItemFields = [
142
+ "System.Id",
143
+ "System.Title",
144
+ "System.Description",
145
+ "System.State",
146
+ "System.Tags",
147
+ "System.CreatedBy",
148
+ "System.ChangedDate",
149
+ "Microsoft.VSTS.Common.AcceptanceCriteria"
150
+ ];
151
+ export const wiqlFor = (filter) => {
152
+ const clauses = [
153
+ "[System.TeamProject] = @project",
154
+ ...(filter.state === "all"
155
+ ? []
156
+ : filter.state === "closed"
157
+ ? ["[System.State] = 'Closed'"]
158
+ : ["[System.State] <> 'Closed'"]),
159
+ ...(filter.tags ?? []).map((tag) => `[System.Tags] CONTAINS ${quoteWiql(tag)}`),
160
+ ...(filter.assignedTo === undefined
161
+ ? []
162
+ : [`[System.AssignedTo] = ${quoteWiql(filter.assignedTo)}`])
163
+ ];
164
+ const select = workItemFields.map((name) => `[${name}]`).join(", ");
165
+ return (`SELECT TOP ${String(filter.limit ?? 100)} ${select} FROM WorkItems ` +
166
+ `WHERE ${clauses.join(" AND ")} ORDER BY [System.Id] ASC`);
167
+ };
168
+ export const queryArgs = (config, wiql) => [
169
+ "boards",
170
+ "query",
171
+ "--wiql",
172
+ wiql,
173
+ "--project",
174
+ config.project,
175
+ ...org(config),
176
+ ...json
177
+ ];
178
+ // ---------------------------------------------------------------------------
179
+ // Pull request argv
180
+ // ---------------------------------------------------------------------------
181
+ export const prCreateArgs = (config, sourceRef, targetRef, title, description, draft = false) => [
182
+ "repos",
183
+ "pr",
184
+ "create",
185
+ "--repository",
186
+ config.repository,
187
+ "--project",
188
+ config.project,
189
+ "--source-branch",
190
+ branchName(sourceRef),
191
+ "--target-branch",
192
+ branchName(targetRef),
193
+ "--title",
194
+ title,
195
+ "--description",
196
+ description,
197
+ ...(draft ? ["--draft", "true"] : []),
198
+ ...org(config),
199
+ ...json
200
+ ];
201
+ export const prListArgs = (config, sourceRef) => [
202
+ "repos",
203
+ "pr",
204
+ "list",
205
+ "--repository",
206
+ config.repository,
207
+ "--project",
208
+ config.project,
209
+ "--status",
210
+ "active",
211
+ ...(sourceRef === undefined ? [] : ["--source-branch", branchName(sourceRef)]),
212
+ ...org(config),
213
+ ...json
214
+ ];
215
+ export const prUpdateArgs = (config, id, title, description) => [
216
+ "repos",
217
+ "pr",
218
+ "update",
219
+ "--id",
220
+ String(id),
221
+ "--title",
222
+ title,
223
+ "--description",
224
+ description,
225
+ ...org(config),
226
+ ...json
227
+ ];
228
+ export const prCommentArgs = (config, id, text) => [
229
+ "repos",
230
+ "pr",
231
+ "thread",
232
+ "create",
233
+ "--id",
234
+ String(id),
235
+ "--content",
236
+ text,
237
+ "--project",
238
+ config.project,
239
+ ...org(config),
240
+ ...json
241
+ ];
242
+ // ---------------------------------------------------------------------------
243
+ // Development links
244
+ // ---------------------------------------------------------------------------
245
+ // The "Development" section of a work item is a set of ArtifactLink
246
+ // relations pointing at git objects. Their URLs are `vstfs:` URIs carrying
247
+ // GUIDs, not names — which is why `repository` below exists: a caller that
248
+ // knows a repository by name has to resolve its id before it can link
249
+ // anything to it, and has to reverse the mapping to read a link back.
250
+ export const GitArtifactKind = Schema.Literals(["Branch", "PullRequest", "Commit"]);
251
+ export class GitArtifact extends Schema.Class("GitArtifact")({
252
+ kind: GitArtifactKind,
253
+ projectId: Schema.String,
254
+ repositoryId: Schema.String,
255
+ // Branch name, pull request id, or commit sha, by kind.
256
+ value: Schema.String
39
257
  }) {
40
258
  }
41
- const encodeBase64 = (value) => {
42
- const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
43
- const bytes = new TextEncoder().encode(value);
44
- let encoded = "";
45
- for (let index = 0; index < bytes.length; index += 3) {
46
- const first = bytes[index] ?? 0;
47
- const second = bytes[index + 1];
48
- const third = bytes[index + 2];
49
- const bits = (first << 16) | ((second ?? 0) << 8) | (third ?? 0);
50
- encoded += alphabet[(bits >>> 18) & 63] ?? "";
51
- encoded += alphabet[(bits >>> 12) & 63] ?? "";
52
- encoded += second === undefined ? "=" : (alphabet[(bits >>> 6) & 63] ?? "");
53
- encoded += third === undefined ? "=" : (alphabet[bits & 63] ?? "");
259
+ export const gitArtifactKinds = ["Branch", "PullRequest", "Commit"];
260
+ // The CLI and the REST payloads name these links in prose, not by kind.
261
+ const linkNames = {
262
+ Branch: "Branch",
263
+ PullRequest: "Pull Request",
264
+ Commit: "Fixed in Commit"
265
+ };
266
+ export const artifactLinkName = (kind) => linkNames[kind];
267
+ export const artifactKindOfName = (name) => gitArtifactKinds.find((kind) => linkNames[kind].toLowerCase() === name.trim().toLowerCase());
268
+ const uriSegment = {
269
+ Branch: "Ref",
270
+ PullRequest: "PullRequestId",
271
+ Commit: "Commit"
272
+ };
273
+ // `vstfs:///Git/Ref/{project}%2F{repo}%2FGB{branch}` — the whole
274
+ // project/repo/value triple is ONE percent-encoded segment, which is what
275
+ // lets a branch name contain slashes without splitting the URI.
276
+ export const artifactUri = (artifact) => {
277
+ const value = artifact.kind === "Branch" ? `GB${artifact.value}` : artifact.value;
278
+ return (`vstfs:///Git/${uriSegment[artifact.kind]}/` +
279
+ encodeURIComponent(`${artifact.projectId}/${artifact.repositoryId}/${value}`));
280
+ };
281
+ export const parseArtifactUri = (uri) => {
282
+ const match = /^vstfs:\/\/\/Git\/(Ref|PullRequestId|Commit)\/(.+)$/.exec(uri.trim());
283
+ const segment = match?.[1];
284
+ const encoded = match?.[2];
285
+ if (segment === undefined || encoded === undefined) {
286
+ return undefined;
287
+ }
288
+ const kind = gitArtifactKinds.find((candidate) => uriSegment[candidate] === segment);
289
+ let decoded;
290
+ try {
291
+ decoded = decodeURIComponent(encoded);
292
+ }
293
+ catch {
294
+ // A malformed escape is a link we cannot act on, not a crash.
295
+ return undefined;
54
296
  }
55
- return encoded;
297
+ // Split into exactly three: a branch name may contain further slashes.
298
+ const first = decoded.indexOf("/");
299
+ const second = decoded.indexOf("/", first + 1);
300
+ if (kind === undefined || first < 0 || second < 0) {
301
+ return undefined;
302
+ }
303
+ const rest = decoded.slice(second + 1);
304
+ const value = kind === "Branch" ? (rest.startsWith("GB") ? rest.slice(2) : rest) : rest;
305
+ return value.length === 0
306
+ ? undefined
307
+ : GitArtifact.make({
308
+ kind,
309
+ projectId: decoded.slice(0, first),
310
+ repositoryId: decoded.slice(first + 1, second),
311
+ value
312
+ });
56
313
  };
57
- export const authorizationHeader = (config) => ({
58
- Authorization: `Basic ${encodeBase64(`:${Redacted.value(config.pat)}`)}`
314
+ export const relationAddArgs = (config, id, artifact) => [
315
+ "boards",
316
+ "work-item",
317
+ "relation",
318
+ "add",
319
+ "--id",
320
+ String(id),
321
+ "--relation-type",
322
+ artifactLinkName(artifact.kind),
323
+ "--target-url",
324
+ artifactUri(artifact),
325
+ ...org(config),
326
+ ...json
327
+ ];
328
+ export const repositoryShowArgs = (config, repository) => [
329
+ "repos",
330
+ "show",
331
+ "--repository",
332
+ repository,
333
+ "--project",
334
+ config.project,
335
+ ...org(config),
336
+ ...json
337
+ ];
338
+ export class GitRepository extends Schema.Class("GitRepository")({
339
+ id: Schema.String,
340
+ name: Schema.String,
341
+ projectId: Schema.String,
342
+ projectName: Schema.String,
343
+ defaultBranch: Schema.String,
344
+ webUrl: Schema.String
345
+ }) {
346
+ }
347
+ export const prPolicyArgs = (config, id) => [
348
+ "repos",
349
+ "pr",
350
+ "policy",
351
+ "list",
352
+ "--id",
353
+ String(id),
354
+ ...org(config),
355
+ ...json
356
+ ];
357
+ export const prCompleteArgs = (config, id, squash, deleteSourceBranch) => [
358
+ "repos",
359
+ "pr",
360
+ "update",
361
+ "--id",
362
+ String(id),
363
+ "--status",
364
+ "completed",
365
+ "--squash",
366
+ squash ? "true" : "false",
367
+ "--delete-source-branch",
368
+ deleteSourceBranch ? "true" : "false",
369
+ ...org(config),
370
+ ...json
371
+ ];
372
+ // ---------------------------------------------------------------------------
373
+ // Parsing
374
+ // ---------------------------------------------------------------------------
375
+ // System.CreatedBy is an identity object on current API versions and a bare
376
+ // display string on older ones; both decode to the display name.
377
+ const Identity = Schema.Union([
378
+ Schema.String,
379
+ Schema.Struct({
380
+ displayName: Schema.optionalKey(Schema.String),
381
+ uniqueName: Schema.optionalKey(Schema.String)
382
+ })
383
+ ]);
384
+ const identityName = (value) => value === undefined
385
+ ? ""
386
+ : typeof value === "string"
387
+ ? value
388
+ : (value.displayName ?? value.uniqueName ?? "");
389
+ const AdoFields = Schema.Struct({
390
+ "System.Title": Schema.optionalKey(Schema.String),
391
+ "System.Description": Schema.optionalKey(Schema.String),
392
+ "System.State": Schema.optionalKey(Schema.String),
393
+ "System.Tags": Schema.optionalKey(Schema.String),
394
+ "System.CreatedBy": Schema.optionalKey(Identity),
395
+ "System.ChangedDate": Schema.optionalKey(Schema.String),
396
+ "Microsoft.VSTS.Common.AcceptanceCriteria": Schema.optionalKey(Schema.String)
59
397
  });
60
- const witBase = (config) => `${config.orgUrl}/${config.project}/_apis/wit`;
61
- const gitBase = (config) => `${config.orgUrl}/${config.project}/_apis/git/repositories/${config.repository}`;
62
- const patchOperations = (fields) => Object.entries(fields).map(([name, value]) => ({
63
- op: "add",
64
- path: `/fields/${name}`,
65
- value
66
- }));
67
- export const readWorkItemRequest = (config, id) => AdoRequest.make({
68
- method: "GET",
69
- url: `${witBase(config)}/workitems/${id}?$expand=relations&api-version=${config.apiVersion}`
398
+ const AdoWorkItem = Schema.Struct({
399
+ id: Schema.Int,
400
+ fields: AdoFields
70
401
  });
71
- export const wiqlRequest = (config, query) => AdoRequest.make({
72
- method: "POST",
73
- url: `${witBase(config)}/wiql?api-version=${config.apiVersion}`,
74
- body: JSON.stringify({ query })
402
+ export const parseTags = (raw) => raw
403
+ .split(";")
404
+ .map((tag) => tag.trim())
405
+ .filter((tag) => tag.length > 0);
406
+ const toWorkItem = (item) => WorkItem.make({
407
+ id: item.id,
408
+ title: item.fields["System.Title"] ?? "",
409
+ description: item.fields["System.Description"] ?? "",
410
+ acceptanceCriteria: item.fields["Microsoft.VSTS.Common.AcceptanceCriteria"] ?? "",
411
+ state: item.fields["System.State"] ?? "",
412
+ tags: parseTags(item.fields["System.Tags"] ?? ""),
413
+ createdBy: identityName(item.fields["System.CreatedBy"]),
414
+ changedDate: item.fields["System.ChangedDate"] ?? ""
75
415
  });
76
- export const setFieldsRequest = (config, id, fields) => AdoRequest.make({
77
- method: "PATCH",
78
- url: `${witBase(config)}/workitems/${id}?api-version=${config.apiVersion}`,
79
- body: JSON.stringify(patchOperations(fields)),
80
- contentType: "application/json-patch+json"
416
+ const decodeFailure = (message) => (error) => ProcessError.make({ message, detail: String(error) });
417
+ export const parseWorkItem = (payload) => Schema.decodeUnknownEffect(Schema.fromJsonString(AdoWorkItem))(payload).pipe(Effect.map(toWorkItem), Effect.mapError(decodeFailure("az boards work-item show")));
418
+ // `az boards query` flattens the WIQL result into a work-item array, so a
419
+ // queue poll is a single call rather than a fan-out over ids.
420
+ export const parseWorkItems = (payload) => Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Array(AdoWorkItem)))(payload).pipe(Effect.map((items) => items.map(toWorkItem)), Effect.mapError(decodeFailure("az boards query")));
421
+ export const parseWorkItemIds = (payload) => parseWorkItems(payload).pipe(Effect.map((items) => items.map((item) => item.id)));
422
+ const AdoComments = Schema.Struct({
423
+ comments: Schema.Array(Schema.Struct({
424
+ id: Schema.Int,
425
+ text: Schema.optionalKey(Schema.String),
426
+ createdBy: Schema.optionalKey(Identity),
427
+ createdDate: Schema.optionalKey(Schema.String)
428
+ })).pipe(Schema.withConstructorDefault(Effect.succeed(Object.freeze([]))))
81
429
  });
82
- export const createPullRequestRequest = (config, sourceRef, targetRef, title, description) => AdoRequest.make({
83
- method: "POST",
84
- url: `${gitBase(config)}/pullrequests?api-version=${config.apiVersion}`,
85
- body: JSON.stringify({
86
- sourceRefName: sourceRef,
87
- targetRefName: targetRef,
88
- title,
89
- description
90
- })
430
+ export const parseComments = (payload) => Schema.decodeUnknownEffect(Schema.fromJsonString(AdoComments))(payload).pipe(Effect.map((parsed) => parsed.comments.map((comment) => WorkItemComment.make({
431
+ id: comment.id,
432
+ author: identityName(comment.createdBy),
433
+ text: comment.text ?? "",
434
+ createdDate: comment.createdDate ?? ""
435
+ }))), Effect.mapError(decodeFailure("az devops invoke wit comments")));
436
+ // `relations` is absent — not empty — on a work item whose Development
437
+ // section has never been touched, which is every work item until this tool
438
+ // links one. optionalKey, because a constructor default does not apply on
439
+ // decode and a missing key would fail the normal case.
440
+ const AdoRelations = Schema.Struct({
441
+ relations: Schema.optionalKey(Schema.Array(Schema.Struct({
442
+ rel: Schema.optionalKey(Schema.String),
443
+ url: Schema.optionalKey(Schema.String),
444
+ attributes: Schema.optionalKey(Schema.Struct({ name: Schema.optionalKey(Schema.String) }))
445
+ })))
91
446
  });
92
- const field = (fields, name) => {
93
- const value = fields[name];
94
- return typeof value === "string" ? value : "";
95
- };
96
- export const parseWorkItem = (json) => Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Struct({
97
- id: Schema.Int,
98
- fields: Schema.Record(Schema.String, Schema.Json)
99
- })))(json).pipe(Effect.map((item) => {
100
- const tags = field(item.fields, "System.Tags")
101
- .split(";")
102
- .map((tag) => tag.trim())
103
- .filter((tag) => tag.length > 0);
104
- return WorkItem.make({
105
- id: item.id,
106
- title: field(item.fields, "System.Title"),
107
- description: field(item.fields, "System.Description"),
108
- acceptanceCriteria: field(item.fields, "Microsoft.VSTS.Common.AcceptanceCriteria"),
109
- state: field(item.fields, "System.State"),
110
- tags
111
- });
112
- }), Effect.mapError((error) => ProcessError.make({
113
- message: "ado parse work item",
114
- detail: String(error)
115
- })));
116
- export const parseWiqlIds = (json) => Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Struct({
117
- workItems: Schema.Array(Schema.Struct({
118
- id: Schema.Int
119
- }))
120
- })))(json).pipe(Effect.map((result) => result.workItems.map((item) => item.id)), Effect.mapError((error) => ProcessError.make({
121
- message: "ado parse wiql",
122
- detail: String(error)
123
- })));
124
- export const parsePullRequest = (config, json) => Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Struct({
447
+ // A work item with no Development section decodes to an empty list rather
448
+ // than failing: "nothing linked yet" is the normal state, not an error.
449
+ export const parseDevelopmentLinks = (payload) => Schema.decodeUnknownEffect(Schema.fromJsonString(AdoRelations))(payload).pipe(Effect.map((parsed) => (parsed.relations ?? [])
450
+ .filter((relation) => (relation.rel ?? "").toLowerCase() === "artifactlink")
451
+ .flatMap((relation) => {
452
+ const artifact = parseArtifactUri(relation.url ?? "");
453
+ if (artifact === undefined) {
454
+ return [];
455
+ }
456
+ // The URI segment already fixes the kind; the attribute name is
457
+ // only a cross-check for the links whose segment is shared.
458
+ const named = artifactKindOfName(relation.attributes?.name ?? "");
459
+ return named === undefined || named === artifact.kind ? [artifact] : [];
460
+ })), Effect.mapError(decodeFailure("az boards work-item show --expand relations")));
461
+ const AdoRepository = Schema.Struct({
462
+ id: Schema.String,
463
+ name: Schema.String,
464
+ project: Schema.Struct({
465
+ id: Schema.String,
466
+ name: Schema.optionalKey(Schema.String)
467
+ }),
468
+ defaultBranch: Schema.optionalKey(Schema.String),
469
+ webUrl: Schema.optionalKey(Schema.String)
470
+ });
471
+ export const parseRepository = (payload) => Schema.decodeUnknownEffect(Schema.fromJsonString(AdoRepository))(payload).pipe(Effect.map((repository) => GitRepository.make({
472
+ id: repository.id,
473
+ name: repository.name,
474
+ projectId: repository.project.id,
475
+ projectName: repository.project.name ?? "",
476
+ // Reported as a full ref; callers branch and push by short name.
477
+ defaultBranch: branchName(repository.defaultBranch ?? ""),
478
+ webUrl: repository.webUrl ?? ""
479
+ })), Effect.mapError(decodeFailure("az repos show")));
480
+ const AdoPr = Schema.Struct({
125
481
  pullRequestId: Schema.Int,
126
482
  repository: Schema.Struct({
127
483
  id: Schema.String,
128
- project: Schema.Struct({
129
- id: Schema.String
130
- })
484
+ project: Schema.Struct({ id: Schema.String })
131
485
  })
132
- })))(json).pipe(Effect.map((result) => AdoPullRequest.make({
133
- id: result.pullRequestId,
134
- repoId: result.repository.id,
135
- projectId: result.repository.project.id,
486
+ });
487
+ const toPullRequest = (config, pr) => AdoPullRequest.make({
488
+ id: pr.pullRequestId,
489
+ repoId: pr.repository.id,
490
+ projectId: pr.repository.project.id,
136
491
  webUrl: `${config.orgUrl}/${config.project}/_git/` +
137
- `${config.repository}/pullrequest/${result.pullRequestId}`
138
- })), Effect.mapError((error) => ProcessError.make({
139
- message: "ado parse pull request",
140
- detail: String(error)
141
- })));
142
- export const makeAzureDevOpsTool = (config, http, events, timeout = Duration.seconds(30)) => {
143
- const run = (request) => http
144
- .send(request.method, request.url, request.body, authorizationHeader(config), request.contentType, timeout)
145
- .pipe(Effect.mapError((error) => ProcessError.make({
146
- message: `ado ${request.method} ${request.url}`,
147
- detail: error.message
148
- })));
492
+ `${config.repository}/pullrequest/${String(pr.pullRequestId)}`
493
+ });
494
+ export const parsePullRequest = (config, payload) => Schema.decodeUnknownEffect(Schema.fromJsonString(AdoPr))(payload).pipe(Effect.map((pr) => toPullRequest(config, pr)), Effect.mapError(decodeFailure("az repos pr")));
495
+ export const parsePullRequests = (config, payload) => Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Array(AdoPr)))(payload).pipe(Effect.map((prs) => prs.map((pr) => toPullRequest(config, pr))), Effect.mapError(decodeFailure("az repos pr list")));
496
+ const AdoPolicies = Schema.Array(Schema.Struct({
497
+ status: Schema.optionalKey(Schema.String)
498
+ }));
499
+ // Policy evaluation statuses: queued/running are still deciding, and
500
+ // rejected/broken have already decided against the PR.
501
+ export const outcomeFromPolicies = (payload) => Schema.decodeUnknownEffect(Schema.fromJsonString(AdoPolicies))(payload).pipe(Effect.map((policies) => {
502
+ const statuses = policies.map((policy) => policy.status?.toLowerCase() ?? "");
503
+ return statuses.some((value) => ["queued", "running"].includes(value))
504
+ ? "Pending"
505
+ : statuses.some((value) => ["rejected", "broken"].includes(value))
506
+ ? "Failure"
507
+ : "Success";
508
+ }), Effect.mapError(decodeFailure("az repos pr policy list")));
509
+ const output = (result) => result.stdout.join("\n").trim();
510
+ export const mergeTags = (current, add, remove) => {
511
+ const removed = new Set(remove.map((tag) => tag.toLowerCase()));
512
+ const kept = current.filter((tag) => !removed.has(tag.toLowerCase()));
513
+ const present = new Set(kept.map((tag) => tag.toLowerCase()));
514
+ const added = add.filter((tag) => !present.has(tag.toLowerCase()) && !removed.has(tag.toLowerCase()));
515
+ return [...kept, ...added];
516
+ };
517
+ export const makeAzureDevOpsTool = (config, process, workDir, events) => {
518
+ const run = (args) => process.run(["az", ...args], workDir, {}).pipe(Effect.mapError((error) => ProcessError.make({ message: `az ${args.join(" ")}`, detail: error.message })), Effect.flatMap((result) => result.exitCode === 0
519
+ ? Effect.succeed(output(result))
520
+ : Effect.fail(ProcessError.make({
521
+ message: `az ${args.join(" ")}`,
522
+ detail: [...result.stdout, ...result.stderr].join("\n").trim() ||
523
+ `exit code ${result.exitCode}`
524
+ }))));
149
525
  const read = (operation, effect) => guarded(Capabilities.AdoRead, operation, events, effect);
150
526
  const write = (operation, effect) => guarded(Capabilities.AdoWrite, operation, events, effect);
151
- const setFields = (id, fields) => write("ado setFields", run(setFieldsRequest(config, id, fields)).pipe(Effect.asVoid));
527
+ const readWorkItem = (id) => read("ado readWorkItem", run(workItemShowArgs(config, id)).pipe(Effect.flatMap(parseWorkItem)));
528
+ const setFields = (id, fields) => write("ado setFields", run(workItemUpdateArgs(config, id, fields)).pipe(Effect.asVoid));
529
+ const listPrs = (sourceRef) => run(prListArgs(config, sourceRef)).pipe(Effect.flatMap((payload) => parsePullRequests(config, payload)));
152
530
  return {
153
- readWorkItem: (id) => read("ado readWorkItem", run(readWorkItemRequest(config, id)).pipe(Effect.flatMap(parseWorkItem))),
154
- wiqlIds: (query) => read("ado wiql", run(wiqlRequest(config, query)).pipe(Effect.flatMap(parseWiqlIds))),
531
+ readWorkItem,
532
+ listWorkItems: (filter = {}) => read("ado listWorkItems", run(queryArgs(config, wiqlFor(filter))).pipe(Effect.flatMap(parseWorkItems))),
533
+ wiqlIds: (query) => read("ado wiql", run(queryArgs(config, query)).pipe(Effect.flatMap(parseWorkItemIds))),
534
+ readComments: (id) => read("ado readComments", run(commentsArgs(config, id)).pipe(Effect.flatMap(parseComments))),
535
+ developmentLinks: (id) => read("ado developmentLinks", run(workItemShowArgs(config, id, "relations")).pipe(Effect.flatMap(parseDevelopmentLinks))),
536
+ linkArtifact: (id, artifact) => write("ado linkArtifact", run(relationAddArgs(config, id, artifact)).pipe(Effect.asVoid)),
537
+ repository: (name = config.repository) => read("ado repository", run(repositoryShowArgs(config, name)).pipe(Effect.flatMap(parseRepository))),
155
538
  setFields,
156
539
  setState: (id, state) => setFields(id, { "System.State": state }),
157
- setAcceptanceCriteria: (id, text) => setFields(id, {
158
- "Microsoft.VSTS.Common.AcceptanceCriteria": text
159
- }),
160
- createPr: (sourceRef, targetRef, title, body) => write("ado createPr", run(createPullRequestRequest(config, sourceRef, targetRef, title, body)).pipe(Effect.flatMap((json) => parsePullRequest(config, json))))
540
+ setAcceptanceCriteria: (id, text) => setFields(id, { "Microsoft.VSTS.Common.AcceptanceCriteria": text }),
541
+ editTags: (id, add, remove) => add.length === 0 && remove.length === 0
542
+ ? Effect.void
543
+ : readWorkItem(id).pipe(Effect.flatMap((item) => {
544
+ const next = mergeTags(item.tags, add, remove);
545
+ return next.length === item.tags.length &&
546
+ next.every((tag, index) => tag === item.tags[index])
547
+ ? Effect.void
548
+ : setFields(id, { "System.Tags": next.join("; ") });
549
+ })),
550
+ writeComment: (id, text) => write("ado writeComment", run(workItemCommentArgs(config, id, text)).pipe(Effect.asVoid)),
551
+ createWorkItem: (workItemType, title, description, tags = []) => write("ado createWorkItem", run(workItemCreateArgs(config, workItemType, title, description, tags)).pipe(Effect.flatMap(parseWorkItem))),
552
+ createPr: (sourceRef, targetRef, title, body, draft = false) => write("ado createPr",
553
+ // An active PR for the branch already IS the deliverable; creating a
554
+ // second one would fail on the server and lose the first's reviews.
555
+ Effect.flatMap(listPrs(sourceRef), (existing) => {
556
+ const open = existing[0];
557
+ return open !== undefined
558
+ ? Effect.succeed(open)
559
+ : run(prCreateArgs(config, sourceRef, targetRef, title, body, draft)).pipe(Effect.flatMap((payload) => parsePullRequest(config, payload)));
560
+ })),
561
+ openPrForBranch: (sourceRef) => read("ado listPrs", listPrs(sourceRef).pipe(Effect.map((prs) => prs[0]))),
562
+ updatePr: (pr, title, body) => write("ado updatePr", run(prUpdateArgs(config, pr.id, title, body)).pipe(Effect.asVoid)),
563
+ writePrComment: (pr, body) => write("ado writePrComment", run(prCommentArgs(config, pr.id, body)).pipe(Effect.asVoid)),
564
+ prPolicies: (pr) => read("ado prPolicies", run(prPolicyArgs(config, pr.id)).pipe(Effect.flatMap(outcomeFromPolicies))),
565
+ completePr: (pr, squash = true, deleteSourceBranch = true) => write("ado completePr", run(prCompleteArgs(config, pr.id, squash, deleteSourceBranch)).pipe(Effect.asVoid))
161
566
  };
162
567
  };
163
568
  //# sourceMappingURL=AzureDevOpsTool.js.map