@usepipr/runtime 0.4.1 → 0.4.3

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,5 +1,5 @@
1
1
  import { c as resolveReadAtRefRequest, d as isRecord, l as unavailableReadAtRefResult, n as boundedLineSlice, r as parseManifestPath, u as createDiffRangeIndex } from "./runtime-tools-core-D8_WsbL_.mjs";
2
- import { a as officialInitRecipeWorkflowEnvSecrets, i as officialInitRecipeFiles, r as officialInitRecipeConfigTs, t as renderOfficialGithubWorkflow } from "./official-github-workflow-Cx-Yhh86.mjs";
2
+ import { a as officialInitRecipeWorkflowEnvSecrets, i as officialInitRecipeFiles, r as officialInitRecipeConfigTs, t as renderOfficialGithubWorkflow } from "./official-github-workflow-Bjlu-FL3.mjs";
3
3
  import { t as isPublishableSuggestedFixSelection } from "./suggested-fix-publication-policy-B5Wwuudp.mjs";
4
4
  import { createRequire } from "node:module";
5
5
  import { chmod, chown, cp, lstat, mkdir, mkdtemp, readdir, rm } from "node:fs/promises";
@@ -597,7 +597,7 @@ function compareStableSemver(left, right) {
597
597
  }
598
598
  //#endregion
599
599
  //#region src/shared/version.ts
600
- const runtimeVersion = "0.4.1";
600
+ const runtimeVersion = "0.4.3";
601
601
  //#endregion
602
602
  //#region src/config/version-compat.ts
603
603
  async function resolveConfigVersionCompatibility(options) {
@@ -928,10 +928,6 @@ function assertRequiredProviderEnv(providers, options) {
928
928
  if (missing.length > 0) throw new Error(`Missing provider env vars: ${missing.map((provider) => provider.apiKeyEnv).join(", ")}`);
929
929
  }
930
930
  //#endregion
931
- //#region src/config/scaffold-versions.ts
932
- const defaultTypesBunVersion = "1.3.14";
933
- const defaultTypescriptVersion = "6.0.3";
934
- //#endregion
935
931
  //#region src/config/init.ts
936
932
  const supportedOfficialInitAdapters = [
937
933
  "github",
@@ -939,8 +935,8 @@ const supportedOfficialInitAdapters = [
939
935
  "azure-devops",
940
936
  "bitbucket"
941
937
  ];
942
- const defaultGitLabImageRef = "ghcr.io/somus/pipr:v0.4.1";
943
- const defaultSdkVersion = "0.4.1";
938
+ const defaultGitLabImageRef = "ghcr.io/somus/pipr:v0.4.3";
939
+ const defaultSdkVersion = "0.4.3";
944
940
  function resolveOfficialInitAdapters(adapters) {
945
941
  if (adapters === void 0) return ["github"];
946
942
  if (adapters.length === 0) return [];
@@ -974,11 +970,7 @@ async function initOfficialMinimalProject(options) {
974
970
  const result = await writeTargets(targets, existing, { skipExisting: false });
975
971
  if (!minimal) {
976
972
  await assertBunAvailable();
977
- const install = Bun.spawn([
978
- "bun",
979
- "install",
980
- "--ignore-scripts"
981
- ], {
973
+ const install = Bun.spawn(initInstallCommand(), {
982
974
  cwd: projectDir,
983
975
  env: process.env,
984
976
  stdout: "pipe",
@@ -1000,6 +992,15 @@ async function initOfficialMinimalProject(options) {
1000
992
  ...result
1001
993
  };
1002
994
  }
995
+ function initInstallCommand(env = process.env) {
996
+ const command = [
997
+ "bun",
998
+ "install",
999
+ "--ignore-scripts"
1000
+ ];
1001
+ if (env.PIPR_INTERNAL_INIT_OFFLINE === "1") command.push("--offline");
1002
+ return command;
1003
+ }
1003
1004
  async function starterFiles(relativeConfigDir, adapters, recipe, minimal = false) {
1004
1005
  const files = [{
1005
1006
  relativePath: path.join(relativeConfigDir, "config.ts"),
@@ -1086,15 +1087,18 @@ function starterBitbucketWebhookEnvironment(recipe) {
1086
1087
  lines.push("");
1087
1088
  return lines.join("\n");
1088
1089
  }
1089
- function starterPackageJson() {
1090
- return `${JSON.stringify({
1090
+ function officialInitPackageManifest(env = process.env) {
1091
+ return {
1091
1092
  private: true,
1092
- dependencies: { "@usepipr/sdk": process.env.PIPR_INTERNAL_INIT_SDK_VERSION ?? defaultSdkVersion },
1093
+ dependencies: { "@usepipr/sdk": env.PIPR_INTERNAL_INIT_SDK_VERSION ?? defaultSdkVersion },
1093
1094
  devDependencies: {
1094
- "@types/bun": defaultTypesBunVersion,
1095
- typescript: defaultTypescriptVersion
1095
+ "@types/bun": env.PIPR_INTERNAL_INIT_TYPES_BUN_VERSION ?? "1.3.14",
1096
+ typescript: env.PIPR_INTERNAL_INIT_TYPESCRIPT_VERSION ?? "6.0.3"
1096
1097
  }
1097
- }, null, 2)}\n`;
1098
+ };
1099
+ }
1100
+ function starterPackageJson() {
1101
+ return `${JSON.stringify(officialInitPackageManifest(), null, 2)}\n`;
1098
1102
  }
1099
1103
  async function writeTargets(targets, existing, options) {
1100
1104
  const created = [];
@@ -1150,996 +1154,172 @@ async function maybeLstat(filePath) {
1150
1154
  }
1151
1155
  }
1152
1156
  //#endregion
1153
- //#region src/hosts/http.ts
1154
- var CodeHostHttpError = class extends Error {
1155
- status;
1156
- constructor(message, status) {
1157
- super(message);
1158
- this.name = "CodeHostHttpError";
1159
- this.status = status;
1157
+ //#region src/diff/git.ts
1158
+ function runGit$1(args, cwd, maxBuffer) {
1159
+ const result = Bun.spawnSync(["git", ...args], {
1160
+ cwd,
1161
+ env: process.env,
1162
+ maxBuffer,
1163
+ stderr: "pipe",
1164
+ stdout: "pipe"
1165
+ });
1166
+ if (result.exitCode !== 0) {
1167
+ const failure = result.stderr?.toString().trim() || "unknown error";
1168
+ throw new Error(`git ${args.join(" ")} failed: ${failure}`);
1160
1169
  }
1161
- };
1162
- function createCodeHostHttpClient(options) {
1163
- const fetchRequest = options.fetch ?? fetch;
1164
- const sleep = options.sleep ?? ((milliseconds) => Bun.sleep(milliseconds));
1165
- const successThrottle = options.successThrottle ?? createCodeHostSuccessThrottle(sleep);
1166
- const maxRetries = options.maxRetries ?? 2;
1167
- const requestTimeoutMilliseconds = options.requestTimeoutMilliseconds ?? 3e4;
1168
- const secrets = [...new Headers(options.headers).values()].flatMap((value) => [value, value.split(/\s+/).at(-1)].filter((candidate) => candidate !== void 0 && candidate.length >= 8));
1169
- return { async json(path, schema, init = {}) {
1170
- const method = init.method?.toUpperCase() ?? "GET";
1171
- for (let attempt = 0;; attempt += 1) {
1172
- await successThrottle.wait();
1173
- const timeoutSignal = AbortSignal.timeout(requestTimeoutMilliseconds);
1174
- const response = await fetchRequest(new URL(path, options.baseUrl), {
1175
- ...init,
1176
- headers: {
1177
- ...Object.fromEntries(new Headers(options.headers)),
1178
- ...Object.fromEntries(new Headers(init.headers))
1179
- },
1180
- signal: init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal
1181
- });
1182
- const retryAfter = retryAfterMilliseconds(response.headers.get("Retry-After"));
1183
- if (response.ok) {
1184
- successThrottle.update(response.headers.get("Retry-After"));
1185
- return schema.parse(await response.json());
1186
- }
1187
- if (shouldRetryCodeHostRequest({
1188
- method,
1189
- attempt,
1190
- maxRetries,
1191
- response,
1192
- retryStatuses: options.retryNonIdempotentStatuses
1193
- })) {
1194
- await sleep(retryAfter || 250 * 2 ** attempt);
1195
- continue;
1196
- }
1197
- const body = (await response.text()).slice(0, 1024);
1198
- throw new CodeHostHttpError(redact$1(`Code host request failed (${response.status} ${response.statusText}): ${body}`, secrets), response.status);
1170
+ return result.stdout.toString();
1171
+ }
1172
+ //#endregion
1173
+ //#region src/diff/diff.ts
1174
+ const lockFilePattern = /(^|\/)(bun\.lock|package-lock\.json|pnpm-lock\.yaml|yarn\.lock|Cargo\.lock)$/;
1175
+ const generatedPattern = /(^|\/)(dist|build|coverage|vendor)\//;
1176
+ const maxInlineChangedLines = 1e3;
1177
+ const maxCommentableRangeLines = 5e3;
1178
+ function buildDiffManifest(options) {
1179
+ const mergeBaseSha = runGit$1([
1180
+ "merge-base",
1181
+ options.baseSha,
1182
+ options.headSha
1183
+ ], options.cwd).trim();
1184
+ const diffHead = options.includeWorkingTree ? void 0 : options.headSha;
1185
+ const files = parseNameStatus(runGit$1(buildDiffArgs([
1186
+ "--name-status",
1187
+ "-z",
1188
+ "--find-renames"
1189
+ ], mergeBaseSha, diffHead), options.cwd));
1190
+ const diffStats = getDiffStats(options.cwd, mergeBaseSha, diffHead);
1191
+ const rawPatch = parseRawPatch(runGit$1(buildUnifiedDiffArgs(mergeBaseSha, diffHead, getPreExcludedFiles(files, diffStats)), options.cwd));
1192
+ const parsedDiff = parseUnifiedDiff(rawPatch.patch, rawPatch.filePaths);
1193
+ for (const file of files) {
1194
+ const stats = diffStats.get(file.path);
1195
+ file.additions = stats?.additions ?? 0;
1196
+ file.deletions = stats?.deletions ?? 0;
1197
+ const preExcludedReason = stats?.excludedReason;
1198
+ const parsedFile = parsedDiff.get(file.path);
1199
+ file.hunks = preExcludedReason ? [] : parsedFile?.hunks ?? [];
1200
+ file.commentableRanges = preExcludedReason ? [] : parsedFile?.commentableRanges ?? [];
1201
+ const excludedReason = preExcludedReason ?? getExcludedReason(file);
1202
+ if (excludedReason) {
1203
+ file.hunks = [];
1204
+ file.commentableRanges = [];
1205
+ file.excludedReason = excludedReason;
1199
1206
  }
1200
- } };
1207
+ }
1208
+ return parseDiffManifest({
1209
+ baseSha: options.baseSha,
1210
+ headSha: options.headSha,
1211
+ mergeBaseSha,
1212
+ files
1213
+ });
1201
1214
  }
1202
- function createCodeHostSuccessThrottle(sleep = (milliseconds) => Bun.sleep(milliseconds)) {
1203
- let notBefore = 0;
1204
- let waitInFlight;
1205
- const waitUntilReady = async () => {
1206
- while (true) {
1207
- const deadline = notBefore;
1208
- const remaining = deadline - Date.now();
1209
- if (remaining <= 0) {
1210
- if (notBefore === deadline) notBefore = 0;
1211
- return;
1212
- }
1213
- await sleep(remaining);
1214
- if (notBefore <= deadline) {
1215
- notBefore = 0;
1216
- return;
1217
- }
1215
+ function parseNameStatus(output) {
1216
+ const fields = output.split("\0");
1217
+ const files = [];
1218
+ let index = 0;
1219
+ while (index < fields.length) {
1220
+ const rawStatus = fields[index++];
1221
+ if (!rawStatus) continue;
1222
+ const firstPath = fields[index++] ?? "";
1223
+ const status = parseFileStatus(rawStatus);
1224
+ if (status === "renamed") {
1225
+ const secondPath = fields[index++] ?? "";
1226
+ files.push(baseFile(secondPath || firstPath, status, firstPath));
1227
+ continue;
1218
1228
  }
1219
- };
1220
- return {
1221
- wait() {
1222
- if (waitInFlight) return waitInFlight;
1223
- const currentWait = waitUntilReady().finally(() => {
1224
- if (waitInFlight === currentWait) waitInFlight = void 0;
1229
+ files.push(baseFile(firstPath, status));
1230
+ }
1231
+ return files;
1232
+ }
1233
+ function parseUnifiedDiff(diff, filePaths) {
1234
+ const state = createDiffParserState(filePaths);
1235
+ for (const line of diff.split("\n")) parseUnifiedDiffLine(state, line);
1236
+ finishActiveHunk(state);
1237
+ return state.filesByPath;
1238
+ }
1239
+ function getDiffStats(cwd, baseSha, headSha) {
1240
+ const output = runGit$1(buildDiffArgs([
1241
+ "--numstat",
1242
+ "-z",
1243
+ "--find-renames"
1244
+ ], baseSha, headSha), cwd);
1245
+ const stats = /* @__PURE__ */ new Map();
1246
+ for (const stat of parseNumstat(output)) {
1247
+ if (stat.binary) {
1248
+ stats.set(stat.path, {
1249
+ additions: 0,
1250
+ deletions: 0,
1251
+ excludedReason: "binary diff"
1225
1252
  });
1226
- waitInFlight = currentWait;
1227
- return waitInFlight;
1228
- },
1229
- update(retryAfter) {
1230
- const delay = retryAfterMilliseconds(retryAfter);
1231
- if (delay > 0) notBefore = Math.max(notBefore, Date.now() + delay);
1253
+ continue;
1232
1254
  }
1233
- };
1255
+ stats.set(stat.path, {
1256
+ additions: stat.additions,
1257
+ deletions: stat.deletions,
1258
+ excludedReason: stat.additions + stat.deletions > maxInlineChangedLines ? "oversized diff" : void 0
1259
+ });
1260
+ }
1261
+ return stats;
1234
1262
  }
1235
- function shouldRetryCodeHostRequest(options) {
1236
- return (options.method === "GET" || options.method === "HEAD" || options.retryStatuses?.includes(options.response.status) === true) && options.attempt < options.maxRetries && (options.response.status === 429 || options.response.status >= 500 || options.retryStatuses?.includes(options.response.status) === true);
1263
+ function getPreExcludedFiles(files, stats) {
1264
+ const excluded = /* @__PURE__ */ new Map();
1265
+ for (const [filePath, stat] of stats) if (stat.excludedReason) excluded.set(filePath, stat.excludedReason);
1266
+ for (const file of files) {
1267
+ const excludedReason = excluded.get(file.path) ?? getPathExcludedReason(file);
1268
+ if (!excludedReason) continue;
1269
+ excluded.set(file.path, excludedReason);
1270
+ if (file.previousPath) excluded.set(file.previousPath, excludedReason);
1271
+ }
1272
+ return excluded;
1237
1273
  }
1238
- function retryAfterMilliseconds(value) {
1239
- if (!value) return 0;
1240
- const seconds = Number(value);
1241
- if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
1242
- const date = Date.parse(value);
1243
- return Number.isFinite(date) ? Math.max(0, date - Date.now()) : 0;
1274
+ function buildUnifiedDiffArgs(baseSha, headSha, excludedFiles) {
1275
+ const args = buildDiffArgs([
1276
+ "--raw",
1277
+ "-z",
1278
+ "--patch",
1279
+ "--submodule=short",
1280
+ "--unified=80",
1281
+ "--find-renames"
1282
+ ], baseSha, headSha);
1283
+ if (excludedFiles.size === 0) return args;
1284
+ return [
1285
+ ...args,
1286
+ "--",
1287
+ ".",
1288
+ ...[...excludedFiles.keys()].map((filePath) => `:(exclude,literal)${filePath}`)
1289
+ ];
1244
1290
  }
1245
- function redact$1(value, secrets) {
1246
- let redacted = value;
1247
- for (const secret of secrets) redacted = redacted.split(secret).join("***");
1248
- return redacted;
1291
+ function buildDiffArgs(options, baseSha, headSha) {
1292
+ return headSha ? [
1293
+ "diff",
1294
+ ...options,
1295
+ baseSha,
1296
+ headSha
1297
+ ] : [
1298
+ "diff",
1299
+ ...options,
1300
+ baseSha
1301
+ ];
1249
1302
  }
1250
- //#endregion
1251
- //#region src/hosts/azure-devops/coordinates.ts
1252
- function azureOrganizationFromUrl(value) {
1253
- const url = new URL(value);
1254
- return url.hostname === "dev.azure.com" ? url.pathname.split("/").filter(Boolean)[0] : url.hostname.split(".")[0];
1255
- }
1256
- //#endregion
1257
- //#region src/hosts/azure-devops/client.ts
1258
- const identitySchema = z.looseObject({
1259
- displayName: z.string().optional(),
1260
- providerDisplayName: z.string().optional(),
1261
- uniqueName: z.string().optional(),
1262
- id: z.string().optional(),
1263
- properties: z.looseObject({ Account: z.looseObject({ $value: z.string().optional() }).optional() }).optional()
1264
- }).transform((identity) => {
1265
- const displayName = identity.displayName ?? identity.providerDisplayName;
1266
- const uniqueName = identity.uniqueName ?? identity.properties?.Account?.$value;
1267
- return {
1268
- ...identity,
1269
- ...displayName ? { displayName } : {},
1270
- ...uniqueName ? { uniqueName } : {}
1271
- };
1272
- });
1273
- const commitSchema = z.looseObject({ commitId: z.string().min(1) });
1274
- const forkRepositorySchema = z.looseObject({ remoteUrl: z.string().min(1) });
1275
- const pullRequestSchema$1 = z.looseObject({
1276
- pullRequestId: z.number().int().positive(),
1277
- isDraft: z.boolean().optional(),
1278
- title: z.string(),
1279
- description: z.string().nullish(),
1280
- url: z.string().optional(),
1281
- sourceRefName: z.string().min(1),
1282
- targetRefName: z.string().min(1),
1283
- createdBy: identitySchema.optional(),
1284
- lastMergeSourceCommit: commitSchema,
1285
- lastMergeTargetCommit: commitSchema,
1286
- forkSource: z.looseObject({ repository: forkRepositorySchema }).optional(),
1287
- repository: z.looseObject({
1288
- id: z.string().min(1),
1289
- name: z.string().min(1),
1290
- url: z.string().optional(),
1291
- project: z.looseObject({
1292
- id: z.string().min(1),
1293
- name: z.string().min(1)
1294
- })
1295
- })
1296
- });
1297
- const repositorySchema$1 = z.looseObject({
1298
- id: z.string().min(1),
1299
- name: z.string().min(1),
1300
- project: z.looseObject({
1301
- id: z.string().min(1),
1302
- name: z.string().min(1)
1303
- })
1304
- });
1305
- const iterationSchema = z.looseObject({
1306
- id: z.number().int().positive(),
1307
- sourceRefCommit: commitSchema
1308
- });
1309
- const iterationChangeSchema = z.looseObject({
1310
- changeTrackingId: z.number().int(),
1311
- changeType: z.string(),
1312
- item: z.looseObject({
1313
- path: z.string().min(1),
1314
- originalPath: z.string().min(1).optional()
1315
- })
1316
- });
1317
- const threadCommentSchema = z.looseObject({
1318
- id: z.union([z.number(), z.string()]).transform(String),
1319
- parentCommentId: z.number().int().optional(),
1320
- content: z.string().default(""),
1321
- author: identitySchema.optional(),
1322
- isDeleted: z.boolean().optional()
1323
- });
1324
- const pointSchema = z.looseObject({
1325
- line: z.number().int(),
1326
- offset: z.number().int()
1327
- });
1328
- const threadSchema = z.looseObject({
1329
- id: z.union([z.number(), z.string()]).transform(String),
1330
- status: z.string().optional(),
1331
- comments: z.array(threadCommentSchema).default([]),
1332
- threadContext: z.looseObject({
1333
- filePath: z.string().optional(),
1334
- leftFileStart: pointSchema.nullish(),
1335
- leftFileEnd: pointSchema.nullish(),
1336
- rightFileStart: pointSchema.nullish(),
1337
- rightFileEnd: pointSchema.nullish()
1338
- }).nullish(),
1339
- pullRequestThreadContext: z.looseObject({
1340
- changeTrackingId: z.number().int().optional(),
1341
- iterationContext: z.looseObject({
1342
- firstComparingIteration: z.number().int(),
1343
- secondComparingIteration: z.number().int()
1344
- }).optional()
1345
- }).nullish()
1346
- });
1347
- const statusSchema$1 = z.looseObject({ id: z.union([z.number(), z.string()]).transform(String) });
1348
- const aclSchema = collectionSchema(z.looseObject({
1349
- inheritPermissions: z.boolean().default(true),
1350
- acesDictionary: z.record(z.string(), z.looseObject({
1351
- allow: z.number().int().default(0),
1352
- deny: z.number().int().default(0),
1353
- extendedInfo: z.looseObject({
1354
- effectiveAllow: z.number().int().default(0),
1355
- effectiveDeny: z.number().int().default(0)
1356
- }).optional()
1357
- }))
1358
- }));
1359
- function createAzureDevOpsClient(env = process.env, fetch = globalThis.fetch, options = {}) {
1360
- const organization = env.AZURE_DEVOPS_ORGANIZATION ?? organizationFromCollectionUri$1(env.SYSTEM_COLLECTIONURI);
1361
- const project = env.AZURE_DEVOPS_PROJECT ?? env.SYSTEM_TEAMPROJECT;
1362
- const pat = env.AZURE_DEVOPS_TOKEN;
1363
- const bearerToken = env.AZURE_DEVOPS_BEARER_TOKEN ?? env.SYSTEM_ACCESSTOKEN;
1364
- if (!organization) throw new Error("AZURE_DEVOPS_ORGANIZATION is required for Azure DevOps API calls");
1365
- if (!project) throw new Error("AZURE_DEVOPS_PROJECT or SYSTEM_TEAMPROJECT is required for Azure DevOps API calls");
1366
- if (!pat && !bearerToken) throw new Error("AZURE_DEVOPS_TOKEN, AZURE_DEVOPS_BEARER_TOKEN, or SYSTEM_ACCESSTOKEN is required for Azure DevOps API calls");
1367
- const headers = pat ? { Authorization: `Basic ${Buffer.from(`:${pat}`).toString("base64")}` } : { Authorization: `Bearer ${bearerToken}` };
1368
- const successThrottle = createCodeHostSuccessThrottle(options.sleep);
1369
- const api = createCodeHostHttpClient({
1370
- baseUrl: `https://dev.azure.com/${encodeURIComponent(organization)}/${encodeURIComponent(project)}/_apis/`,
1371
- headers,
1372
- fetch,
1373
- successThrottle
1374
- });
1375
- const organizationApi = createCodeHostHttpClient({
1376
- baseUrl: `https://dev.azure.com/${encodeURIComponent(organization)}/_apis/`,
1377
- headers,
1378
- fetch,
1379
- successThrottle
1380
- });
1381
- const identityApi = createCodeHostHttpClient({
1382
- baseUrl: `https://vssps.dev.azure.com/${encodeURIComponent(organization)}/_apis/`,
1383
- headers,
1384
- fetch,
1385
- successThrottle
1386
- });
1387
- const pullRequestPath = (repositoryId, changeNumber) => `git/repositories/${encodeURIComponent(repositoryId)}/pullRequests/${changeNumber}`;
1388
- return {
1389
- organization,
1390
- project,
1391
- async currentUser() {
1392
- return (await organizationApi.json("connectionData?connectOptions=1&lastChangeId=-1&lastChangeId64=-1", z.looseObject({ authenticatedUser: identitySchema }))).authenticatedUser;
1393
- },
1394
- async getRepository(repository) {
1395
- const value = await api.json(withApiVersion(`git/repositories/${encodeURIComponent(repository)}`), repositorySchema$1);
1396
- return {
1397
- id: value.id,
1398
- name: value.name,
1399
- projectId: value.project.id,
1400
- project: value.project.name
1401
- };
1402
- },
1403
- async getRepositoryPermission(actor, projectId, repositoryId) {
1404
- const candidates = (await identityApi.json(`identities?searchFilter=General&filterValue=${encodeURIComponent(actor)}&queryMembership=ExpandedUp&api-version=7.1`, collectionSchema(z.looseObject({
1405
- descriptor: z.string().min(1),
1406
- isActive: z.boolean().optional(),
1407
- isContainer: z.boolean().optional(),
1408
- memberOf: z.array(z.union([z.string().min(1), z.looseObject({ descriptor: z.string().min(1) })])).default([])
1409
- })))).value.filter((identity) => identity.isActive !== false && identity.isContainer !== true);
1410
- if (candidates.length !== 1) return "none";
1411
- const identity = candidates[0];
1412
- if (!identity) return "none";
1413
- const descriptors = [identity.descriptor, ...identity.memberOf.map((group) => typeof group === "string" ? group : group.descriptor)];
1414
- const loadAcls = (token) => {
1415
- const query = new URLSearchParams({
1416
- token,
1417
- descriptors: descriptors.join(","),
1418
- includeExtendedInfo: "true",
1419
- recurse: "false",
1420
- "api-version": "7.1"
1421
- });
1422
- return organizationApi.json(`accesscontrollists/2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87?${query}`, aclSchema);
1423
- };
1424
- const [projectAcls, repositoryAcls] = await Promise.all([loadAcls(`repoV2/${projectId}`), loadAcls(`repoV2/${projectId}/${repositoryId}`)]);
1425
- return azurePermissionFromAcls(repositoryAcls.value.every((acl) => acl.inheritPermissions) ? [...projectAcls.value, ...repositoryAcls.value] : repositoryAcls.value);
1426
- },
1427
- getPullRequest: (repositoryId, changeNumber) => api.json(withApiVersion(pullRequestPath(repositoryId, changeNumber)), pullRequestSchema$1),
1428
- async listIterations(repositoryId, changeNumber) {
1429
- return (await api.json(withApiVersion(`${pullRequestPath(repositoryId, changeNumber)}/iterations`), collectionSchema(iterationSchema))).value.map((iteration) => ({
1430
- id: iteration.id,
1431
- headSha: iteration.sourceRefCommit.commitId
1432
- }));
1433
- },
1434
- async loadChange(options) {
1435
- if (options.organization !== organization || options.project !== project) throw new Error("Azure DevOps client coordinates do not match its configured organization and project");
1436
- const pullRequest = await this.getPullRequest(options.repositoryId, options.changeNumber);
1437
- const iterations = await this.listIterations(options.repositoryId, options.changeNumber);
1438
- const headSha = pullRequest.lastMergeSourceCommit.commitId;
1439
- const iteration = iterations.findLast((candidate) => candidate.headSha === headSha);
1440
- if (!iteration) throw new Error(`Azure DevOps has no pull request iteration for head ${headSha}`);
1441
- const sourceRef = branchName(pullRequest.sourceRefName);
1442
- const targetRef = branchName(pullRequest.targetRefName);
1443
- return {
1444
- repository: {
1445
- slug: `${organization}/${pullRequest.repository.project.name}/${pullRequest.repository.name}`,
1446
- url: repositoryWebUrl(organization, pullRequest.repository.project.name, pullRequest.repository.name)
1447
- },
1448
- coordinates: {
1449
- provider: "azure-devops",
1450
- organization,
1451
- project: pullRequest.repository.project.name,
1452
- projectId: pullRequest.repository.project.id,
1453
- repositoryId: pullRequest.repository.id
1454
- },
1455
- change: {
1456
- number: pullRequest.pullRequestId,
1457
- isDraft: pullRequest.isDraft,
1458
- title: pullRequest.title,
1459
- description: pullRequest.description ?? "",
1460
- url: `${repositoryWebUrl(organization, pullRequest.repository.project.name, pullRequest.repository.name)}/pullrequest/${pullRequest.pullRequestId}`,
1461
- author: pullRequest.createdBy?.uniqueName ? { login: pullRequest.createdBy.uniqueName } : void 0,
1462
- base: {
1463
- sha: pullRequest.lastMergeTargetCommit.commitId,
1464
- ref: targetRef
1465
- },
1466
- head: {
1467
- sha: headSha,
1468
- ref: sourceRef,
1469
- ...pullRequest.forkSource?.repository.remoteUrl ? { url: pullRequest.forkSource.repository.remoteUrl } : {}
1470
- },
1471
- ...pullRequest.forkSource ? { isFork: true } : {}
1472
- },
1473
- iterationId: iteration.id
1474
- };
1475
- },
1476
- async listIterationChanges(repositoryId, changeNumber, iterationId) {
1477
- const changes = [];
1478
- let skip = 0;
1479
- let top = 2e3;
1480
- for (;;) {
1481
- const path = `${pullRequestPath(repositoryId, changeNumber)}/iterations/${iterationId}/changes?compareTo=0&$skip=${skip}&$top=${top}`;
1482
- const page = await api.json(withApiVersion(path), z.looseObject({
1483
- changeEntries: z.array(iterationChangeSchema),
1484
- nextSkip: z.number().int().nonnegative().optional(),
1485
- nextTop: z.number().int().nonnegative().max(2e3).optional()
1486
- }));
1487
- changes.push(...page.changeEntries.map((change) => ({
1488
- changeTrackingId: change.changeTrackingId,
1489
- changeType: change.changeType,
1490
- path: trimLeadingSlash(change.item.path),
1491
- ...change.item.originalPath ? { originalPath: trimLeadingSlash(change.item.originalPath) } : {}
1492
- })));
1493
- if (!page.nextSkip) return changes;
1494
- skip = page.nextSkip;
1495
- if (page.nextTop) top = page.nextTop;
1496
- }
1497
- },
1498
- async listThreads(repositoryId, changeNumber) {
1499
- return (await api.json(withApiVersion(`${pullRequestPath(repositoryId, changeNumber)}/threads`), collectionSchema(threadSchema))).value;
1500
- },
1501
- createThread: (repositoryId, changeNumber, body) => api.json(withApiVersion(`${pullRequestPath(repositoryId, changeNumber)}/threads`), threadSchema, jsonRequest$2("POST", body)),
1502
- updateComment: (repositoryId, changeNumber, threadId, commentId, content) => api.json(withApiVersion(`${pullRequestPath(repositoryId, changeNumber)}/threads/${encodeURIComponent(threadId)}/comments/${encodeURIComponent(commentId)}`), threadCommentSchema, jsonRequest$2("PATCH", { content })),
1503
- createThreadComment: (repositoryId, changeNumber, threadId, body) => api.json(withApiVersion(`${pullRequestPath(repositoryId, changeNumber)}/threads/${encodeURIComponent(threadId)}/comments`), threadCommentSchema, jsonRequest$2("POST", body)),
1504
- updateThreadStatus: (repositoryId, changeNumber, threadId, status) => api.json(withApiVersion(`${pullRequestPath(repositoryId, changeNumber)}/threads/${encodeURIComponent(threadId)}`), threadSchema, jsonRequest$2("PATCH", { status })),
1505
- async createStatus(repositoryId, changeNumber, body) {
1506
- return (await api.json(withApiVersion(`${pullRequestPath(repositoryId, changeNumber)}/statuses`), statusSchema$1, jsonRequest$2("POST", body))).id;
1507
- }
1508
- };
1509
- }
1510
- function azureDevOpsStatusState(state) {
1511
- switch (state) {
1512
- case "pending": return "pending";
1513
- case "success": return "succeeded";
1514
- case "failure": return "failed";
1515
- case "neutral": return "notApplicable";
1516
- }
1517
- }
1518
- function azureRepositoryPermission(bits) {
1519
- if ((bits & 2) === 0) return "none";
1520
- if ((bits & 8192) !== 0) return "admin";
1521
- if ((bits & 2048) !== 0) return "maintain";
1522
- if ((bits & 4) !== 0) return "write";
1523
- if ((bits & 16384) !== 0) return "triage";
1524
- return "read";
1525
- }
1526
- function azurePermissionFromAcls(acls) {
1527
- let allow = 0;
1528
- let deny = 0;
1529
- for (const acl of acls) for (const ace of Object.values(acl.acesDictionary)) {
1530
- allow |= ace.extendedInfo?.effectiveAllow ?? ace.allow;
1531
- deny |= ace.extendedInfo?.effectiveDeny ?? ace.deny;
1532
- }
1533
- return azureRepositoryPermission(allow & ~deny);
1534
- }
1535
- function collectionSchema(item) {
1536
- return z.looseObject({
1537
- count: z.number().int().nonnegative(),
1538
- value: z.array(item)
1539
- });
1540
- }
1541
- function withApiVersion(path) {
1542
- return `${path}${path.includes("?") ? "&" : "?"}api-version=7.1`;
1543
- }
1544
- function jsonRequest$2(method, body) {
1545
- return {
1546
- method,
1547
- headers: { "Content-Type": "application/json" },
1548
- body: JSON.stringify(body)
1549
- };
1550
- }
1551
- function organizationFromCollectionUri$1(value) {
1552
- return value ? azureOrganizationFromUrl(value) : void 0;
1553
- }
1554
- function branchName(ref) {
1555
- return ref.replace(/^refs\/heads\//, "");
1556
- }
1557
- function repositoryWebUrl(organization, project, repository) {
1558
- return `https://dev.azure.com/${encodeURIComponent(organization)}/${encodeURIComponent(project)}/_git/${encodeURIComponent(repository)}`;
1559
- }
1560
- function trimLeadingSlash(value) {
1561
- return value.replace(/^\/+/, "");
1562
- }
1563
- //#endregion
1564
- //#region src/hosts/bitbucket/schema.ts
1565
- const bitbucketRepositorySchema = z.looseObject({
1566
- uuid: z.string().min(1),
1567
- name: z.string().min(1),
1568
- full_name: z.string().min(1),
1569
- slug: z.string().min(1).optional(),
1570
- links: z.looseObject({ html: z.looseObject({ href: z.string().url() }) })
1571
- }).transform((repository) => ({
1572
- ...repository,
1573
- slug: repository.slug ?? repository.full_name.split("/").at(-1) ?? repository.full_name
1574
- }));
1575
- //#endregion
1576
- //#region src/hosts/bitbucket/client.ts
1577
- const userSchema$1 = z.looseObject({
1578
- uuid: z.string().optional(),
1579
- nickname: z.string().optional(),
1580
- display_name: z.string().optional()
1581
- });
1582
- const endpointSchema = z.looseObject({
1583
- branch: z.looseObject({ name: z.string().min(1) }),
1584
- commit: z.looseObject({ hash: z.string().min(1) }),
1585
- repository: bitbucketRepositorySchema
1586
- });
1587
- const pullRequestSchema = z.looseObject({
1588
- id: z.number().int().positive(),
1589
- draft: z.boolean().optional(),
1590
- title: z.string(),
1591
- description: z.string().default(""),
1592
- author: userSchema$1.optional(),
1593
- source: endpointSchema,
1594
- destination: endpointSchema,
1595
- links: z.looseObject({ html: z.looseObject({ href: z.string().url() }) })
1596
- });
1597
- const inlineSchema = z.looseObject({
1598
- path: z.string().optional(),
1599
- from: z.number().int().nullable().optional(),
1600
- to: z.number().int().nullable().optional(),
1601
- start_from: z.number().int().nullable().optional(),
1602
- start_to: z.number().int().nullable().optional()
1603
- });
1604
- const commentSchema = z.looseObject({
1605
- id: z.union([z.number(), z.string()]).transform(String),
1606
- content: z.looseObject({ raw: z.string().default("") }),
1607
- user: userSchema$1.optional(),
1608
- parent: z.looseObject({ id: z.union([z.number(), z.string()]).transform(String) }).optional(),
1609
- inline: inlineSchema.optional(),
1610
- deleted: z.boolean().optional(),
1611
- resolution: z.looseObject({}).optional()
1612
- });
1613
- function createBitbucketClient(env = process.env, fetch = globalThis.fetch) {
1614
- const workspace = env.BITBUCKET_WORKSPACE;
1615
- const repository = env.BITBUCKET_REPO_SLUG;
1616
- const token = env.BITBUCKET_API_TOKEN;
1617
- const email = env.BITBUCKET_EMAIL;
1618
- if (!workspace) throw new Error("BITBUCKET_WORKSPACE is required for Bitbucket Cloud API calls");
1619
- if (!repository) throw new Error("BITBUCKET_REPO_SLUG is required for Bitbucket Cloud API calls");
1620
- if (!token || !email) throw new Error("BITBUCKET_EMAIL and BITBUCKET_API_TOKEN are required");
1621
- const authorization = `Basic ${Buffer.from(`${email}:${token}`).toString("base64")}`;
1622
- const repositoryApiPath = `/2.0/repositories/${encodeURIComponent(workspace)}/${encodeURIComponent(repository)}/`;
1623
- const api = createCodeHostHttpClient({
1624
- baseUrl: `https://api.bitbucket.org${repositoryApiPath}`,
1625
- headers: { Authorization: authorization },
1626
- fetch
1627
- });
1628
- const rootApi = createCodeHostHttpClient({
1629
- baseUrl: "https://api.bitbucket.org/2.0/",
1630
- headers: { Authorization: authorization },
1631
- fetch
1632
- });
1633
- const prPath = (id) => `pullrequests/${id}`;
1634
- return {
1635
- workspace,
1636
- repository,
1637
- async currentUser() {
1638
- const value = await rootApi.json("user", userSchema$1);
1639
- return {
1640
- uuid: value.uuid,
1641
- nickname: value.nickname,
1642
- displayName: value.display_name
1643
- };
1644
- },
1645
- async getRepository() {
1646
- const value = await api.json("", bitbucketRepositorySchema);
1647
- return {
1648
- uuid: value.uuid,
1649
- slug: value.slug,
1650
- fullName: value.full_name,
1651
- url: value.links.html.href
1652
- };
1653
- },
1654
- async getRepositoryPermission(actor, repositoryUuid) {
1655
- const permissionEmail = env.BITBUCKET_PERMISSION_EMAIL;
1656
- const permissionToken = env.BITBUCKET_PERMISSION_API_TOKEN;
1657
- if (!permissionEmail || !permissionToken) throw new Error("BITBUCKET_PERMISSION_EMAIL and BITBUCKET_PERMISSION_API_TOKEN are required for Bitbucket permission checks");
1658
- const permissionApi = createCodeHostHttpClient({
1659
- baseUrl: "https://api.bitbucket.org/2.0/",
1660
- headers: { Authorization: `Basic ${Buffer.from(`${permissionEmail}:${permissionToken}`).toString("base64")}` },
1661
- fetch
1662
- });
1663
- const query = encodeURIComponent(`repository.uuid="${escapeBitbucketQueryValue(repositoryUuid)}" AND user.nickname="${escapeBitbucketQueryValue(actor)}"`);
1664
- return (await permissionApi.json(`workspaces/${encodeURIComponent(workspace)}/permissions/repositories?q=${query}&pagelen=100`, pagedSchema(z.looseObject({
1665
- permission: z.enum([
1666
- "read",
1667
- "write",
1668
- "admin"
1669
- ]),
1670
- user: userSchema$1
1671
- })))).values.find((entry) => entry.user.nickname === actor)?.permission ?? "none";
1672
- },
1673
- getPullRequest: (id) => api.json(prPath(id), pullRequestSchema),
1674
- async loadChange(options) {
1675
- if (options.workspace !== workspace || options.repository !== repository) throw new Error("Bitbucket client coordinates do not match the requested repository");
1676
- const pullRequest = await this.getPullRequest(options.changeNumber);
1677
- return {
1678
- repository: {
1679
- slug: pullRequest.destination.repository.full_name,
1680
- url: pullRequest.destination.repository.links.html.href
1681
- },
1682
- coordinates: {
1683
- provider: "bitbucket",
1684
- workspace,
1685
- repository,
1686
- repositoryUuid: pullRequest.destination.repository.uuid
1687
- },
1688
- change: {
1689
- number: pullRequest.id,
1690
- isDraft: pullRequest.draft,
1691
- title: pullRequest.title,
1692
- description: pullRequest.description,
1693
- url: pullRequest.links.html.href,
1694
- author: pullRequest.author?.nickname ? { login: pullRequest.author.nickname } : void 0,
1695
- base: {
1696
- sha: pullRequest.destination.commit.hash,
1697
- ref: pullRequest.destination.branch.name,
1698
- url: pullRequest.destination.repository.links.html.href
1699
- },
1700
- head: {
1701
- sha: pullRequest.source.commit.hash,
1702
- ref: pullRequest.source.branch.name,
1703
- url: pullRequest.source.repository.links.html.href
1704
- },
1705
- isFork: pullRequest.source.repository.uuid !== pullRequest.destination.repository.uuid
1706
- }
1707
- };
1708
- },
1709
- async listComments(id) {
1710
- return (await listAll(api, `${prPath(id)}/comments`, commentSchema, repositoryApiPath)).filter((comment) => !comment.deleted);
1711
- },
1712
- createComment: (id, body) => api.json(`${prPath(id)}/comments`, commentSchema, jsonRequest$1("POST", body)),
1713
- updateComment: (id, commentId, content) => api.json(`${prPath(id)}/comments/${encodeURIComponent(commentId)}`, commentSchema, jsonRequest$1("PUT", { content: { raw: content } })),
1714
- async replyToComment(id, commentId, content) {
1715
- const parentId = positiveCommentId(commentId);
1716
- return await api.json(`${prPath(id)}/comments`, commentSchema, jsonRequest$1("POST", {
1717
- content: { raw: content },
1718
- parent: { id: parentId }
1719
- }));
1720
- },
1721
- async resolveComment(id, commentId) {
1722
- positiveCommentId(commentId);
1723
- await api.json(`${prPath(id)}/comments/${encodeURIComponent(commentId)}/resolve`, z.unknown(), { method: "POST" });
1724
- },
1725
- async setStatus(sha, key, body) {
1726
- return (await api.json(`commit/${encodeURIComponent(sha)}/statuses/build`, z.looseObject({ key: z.string().default(key) }), jsonRequest$1("POST", {
1727
- ...body,
1728
- key
1729
- }))).key;
1730
- }
1731
- };
1732
- }
1733
- function positiveCommentId(value) {
1734
- const id = Number(value);
1735
- if (!Number.isSafeInteger(id) || id <= 0) throw new Error("Bitbucket comment ID must be a positive integer");
1736
- return id;
1737
- }
1738
- function bitbucketStatusState(state) {
1739
- if (state === "pending") return "INPROGRESS";
1740
- if (state === "failure") return "FAILED";
1741
- if (state === "neutral") return "STOPPED";
1742
- return "SUCCESSFUL";
1743
- }
1744
- function escapeBitbucketQueryValue(value) {
1745
- return value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"");
1746
- }
1747
- function pagedSchema(item) {
1748
- return z.looseObject({
1749
- values: z.array(item),
1750
- next: z.string().url().optional()
1751
- });
1752
- }
1753
- async function listAll(api, path, schema, allowedPathPrefix) {
1754
- const values = [];
1755
- let next = path;
1756
- while (next) {
1757
- const page = await api.json(next, pagedSchema(schema));
1758
- values.push(...page.values);
1759
- if (page.next) {
1760
- const url = new URL(page.next);
1761
- if (url.origin !== "https://api.bitbucket.org" || !url.pathname.startsWith(allowedPathPrefix)) throw new Error("Bitbucket pagination URL must stay inside the configured repository API");
1762
- }
1763
- next = page.next;
1764
- }
1765
- return values;
1766
- }
1767
- function jsonRequest$1(method, body) {
1768
- return {
1769
- method,
1770
- headers: { "Content-Type": "application/json" },
1771
- body: JSON.stringify(body)
1772
- };
1773
- }
1774
- //#endregion
1775
- //#region src/hosts/gitlab/client.ts
1776
- const userSchema = z.looseObject({
1777
- id: z.number().int().positive(),
1778
- username: z.string().min(1)
1779
- });
1780
- const noteSchema = z.looseObject({
1781
- id: z.union([z.number(), z.string()]).transform(String),
1782
- body: z.string(),
1783
- author: userSchema.optional()
1784
- });
1785
- const positionSchema = z.looseObject({
1786
- new_path: z.string().optional(),
1787
- old_path: z.string().optional(),
1788
- new_line: z.number().int().positive().nullish().transform((value) => value ?? void 0),
1789
- old_line: z.number().int().positive().nullish().transform((value) => value ?? void 0),
1790
- line_range: z.looseObject({
1791
- start: z.looseObject({
1792
- new_line: z.number().int().positive().nullish(),
1793
- old_line: z.number().int().positive().nullish()
1794
- }),
1795
- end: z.looseObject({
1796
- new_line: z.number().int().positive().nullish(),
1797
- old_line: z.number().int().positive().nullish()
1798
- })
1799
- }).optional()
1800
- });
1801
- const discussionNoteSchema = noteSchema.extend({
1802
- resolvable: z.boolean().optional(),
1803
- resolved: z.boolean().optional(),
1804
- position: positionSchema.nullable().optional()
1805
- });
1806
- const discussionSchema = z.looseObject({
1807
- id: z.string().min(1),
1808
- individual_note: z.boolean().optional(),
1809
- notes: z.array(discussionNoteSchema)
1810
- });
1811
- const diffRefsSchema = z.looseObject({
1812
- base_sha: z.string().min(1),
1813
- start_sha: z.string().min(1),
1814
- head_sha: z.string().min(1)
1815
- });
1816
- const mergeRequestResponseSchema = z.looseObject({
1817
- iid: z.number().int().positive(),
1818
- title: z.string().default(""),
1819
- description: z.string().nullable().optional(),
1820
- web_url: z.string().url().optional(),
1821
- source_branch: z.string().min(1),
1822
- target_branch: z.string().min(1),
1823
- source_project_id: z.number().int().positive(),
1824
- target_project_id: z.number().int().positive(),
1825
- sha: z.string().min(1),
1826
- draft: z.boolean().optional(),
1827
- author: userSchema.optional(),
1828
- references: z.looseObject({ full: z.string().optional() }).optional(),
1829
- diff_refs: diffRefsSchema.nullable().optional()
1830
- });
1831
- const mergeRequestSchema = mergeRequestResponseSchema.extend({ diff_refs: diffRefsSchema });
1832
- const memberSchema = z.looseObject({ access_level: z.number().int().min(0) });
1833
- const statusSchema = z.looseObject({ id: z.union([z.number(), z.string()]).optional() });
1834
- function createGitLabClient(env = process.env, fetch = globalThis.fetch, sleep = (milliseconds) => Bun.sleep(milliseconds)) {
1835
- const token = env.GITLAB_TOKEN ?? env.CI_JOB_TOKEN;
1836
- if (!token) throw new Error("GITLAB_TOKEN or CI_JOB_TOKEN is required for GitLab API calls");
1837
- const headers = env.GITLAB_TOKEN ? { "PRIVATE-TOKEN": token } : { "JOB-TOKEN": token };
1838
- const clientOptions = {
1839
- baseUrl: `${(env.CI_API_V4_URL ?? "https://gitlab.com/api/v4").replace(/\/$/, "")}/`,
1840
- headers,
1841
- fetch,
1842
- sleep: async (milliseconds) => void await sleep(milliseconds)
1843
- };
1844
- const api = createCodeHostHttpClient(clientOptions);
1845
- const statusApi = createCodeHostHttpClient({
1846
- ...clientOptions,
1847
- retryNonIdempotentStatuses: [409]
1848
- });
1849
- return {
1850
- async getProject(project) {
1851
- const value = await api.json(`projects/${encodeURIComponent(project)}`, z.looseObject({
1852
- id: z.union([z.number(), z.string()]).transform(String),
1853
- path_with_namespace: z.string().min(1)
1854
- }));
1855
- return {
1856
- id: value.id,
1857
- path: value.path_with_namespace
1858
- };
1859
- },
1860
- currentUser: () => api.json("user", userSchema),
1861
- async loadChange(options) {
1862
- const mergeRequest = await this.getMergeRequest(options.projectId, options.changeNumber);
1863
- return {
1864
- repository: {
1865
- slug: options.projectPath,
1866
- url: projectWebUrl(mergeRequest.web_url)
1867
- },
1868
- coordinates: {
1869
- provider: "gitlab",
1870
- projectId: String(mergeRequest.target_project_id),
1871
- projectPath: options.projectPath
1872
- },
1873
- change: {
1874
- number: mergeRequest.iid,
1875
- title: mergeRequest.title,
1876
- description: mergeRequest.description ?? "",
1877
- url: mergeRequest.web_url,
1878
- author: mergeRequest.author ? { login: mergeRequest.author.username } : void 0,
1879
- base: {
1880
- sha: mergeRequest.diff_refs.start_sha,
1881
- ref: mergeRequest.target_branch
1882
- },
1883
- head: {
1884
- sha: mergeRequest.diff_refs.head_sha,
1885
- ref: mergeRequest.source_branch
1886
- },
1887
- isFork: mergeRequest.source_project_id !== mergeRequest.target_project_id,
1888
- isDraft: mergeRequest.draft
1889
- }
1890
- };
1891
- },
1892
- async getMergeRequest(projectId, changeNumber) {
1893
- const requestPath = `projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}`;
1894
- for (let attempt = 0;; attempt += 1) {
1895
- const response = await api.json(requestPath, mergeRequestResponseSchema);
1896
- const prepared = mergeRequestSchema.safeParse(response);
1897
- if (prepared.success) return prepared.data;
1898
- if (attempt >= 4) throw new Error("GitLab merge request diff refs were not prepared after 5 attempts");
1899
- await sleep(250 * 2 ** attempt);
1900
- }
1901
- },
1902
- async getRepositoryPermission(projectId, username) {
1903
- const user = (await api.json(`users?username=${encodeURIComponent(username)}`, z.array(userSchema))).find((candidate) => candidate.username === username);
1904
- if (!user) return "none";
1905
- try {
1906
- return accessLevelPermission((await api.json(`projects/${encodeURIComponent(projectId)}/members/all/${user.id}`, memberSchema)).access_level);
1907
- } catch (error) {
1908
- if (error instanceof CodeHostHttpError && error.status === 404) return "none";
1909
- throw error;
1910
- }
1911
- },
1912
- listNotes: (projectId, changeNumber) => paginated(api, `projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/notes`, noteSchema),
1913
- createNote: (projectId, changeNumber, body) => api.json(`projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/notes`, noteSchema, jsonRequest("POST", { body })),
1914
- updateNote: (projectId, changeNumber, noteId, body) => api.json(`projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/notes/${encodeURIComponent(noteId)}`, noteSchema, jsonRequest("PUT", { body })),
1915
- listDiscussions: (projectId, changeNumber) => paginated(api, `projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/discussions`, discussionSchema),
1916
- getDiscussion: (projectId, changeNumber, discussionId) => api.json(`projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/discussions/${encodeURIComponent(discussionId)}`, discussionSchema),
1917
- async findReplyParent(projectId, changeNumber, noteId, discussionId) {
1918
- if (!discussionId) return void 0;
1919
- const discussion = await this.getDiscussion(projectId, changeNumber, discussionId);
1920
- if (!discussion.notes.some((note) => note.id === noteId) || discussion.notes[0]?.id === noteId) return;
1921
- return discussion.notes[0]?.id;
1922
- },
1923
- createDiscussion: (projectId, changeNumber, body, position) => api.json(`projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/discussions`, discussionSchema, jsonRequest("POST", {
1924
- body,
1925
- position
1926
- })),
1927
- replyDiscussion: (projectId, changeNumber, discussionId, body) => api.json(`projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/discussions/${encodeURIComponent(discussionId)}/notes`, noteSchema, jsonRequest("POST", { body })),
1928
- async resolveDiscussion(projectId, changeNumber, discussionId) {
1929
- await api.json(`projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/discussions/${encodeURIComponent(discussionId)}`, discussionSchema, jsonRequest("PUT", { resolved: true }));
1930
- },
1931
- async setStatus(projectId, sha, name, state, description) {
1932
- const status = await statusApi.json(`projects/${encodeURIComponent(projectId)}/statuses/${encodeURIComponent(sha)}`, statusSchema, jsonRequest("POST", {
1933
- state: gitLabStatusState(state),
1934
- name,
1935
- description: boundedStatusDescription(description)
1936
- }));
1937
- return String(status.id ?? name);
1938
- }
1939
- };
1940
- }
1941
- const MAX_PAGINATION_PAGES = 100;
1942
- async function paginated(client, path, schema) {
1943
- const values = [];
1944
- for (let page = 1; page <= MAX_PAGINATION_PAGES; page += 1) {
1945
- const separator = path.includes("?") ? "&" : "?";
1946
- const batch = await client.json(`${path}${separator}per_page=100&page=${page}`, z.array(schema));
1947
- values.push(...batch);
1948
- if (batch.length < 100) return values;
1949
- }
1950
- throw new Error(`GitLab pagination exceeded ${MAX_PAGINATION_PAGES} pages for ${path}`);
1951
- }
1952
- function jsonRequest(method, body) {
1953
- return {
1954
- method,
1955
- headers: { "Content-Type": "application/json" },
1956
- body: JSON.stringify(body)
1957
- };
1958
- }
1959
- function accessLevelPermission(level) {
1960
- if (level >= 50) return "admin";
1961
- if (level >= 40) return "maintain";
1962
- if (level >= 30) return "write";
1963
- if (level >= 15) return "triage";
1964
- if (level >= 10) return "read";
1965
- return "none";
1966
- }
1967
- function boundedStatusDescription(description) {
1968
- return description && description.length > 255 ? `${description.slice(0, 252).trimEnd()}...` : description;
1969
- }
1970
- function gitLabStatusState(state) {
1971
- switch (state) {
1972
- case "pending": return "pending";
1973
- case "success": return "success";
1974
- case "failure": return "failed";
1975
- case "neutral": return "skipped";
1976
- }
1977
- }
1978
- function projectWebUrl(mergeRequestUrl) {
1979
- return mergeRequestUrl?.replace(/\/-\/merge_requests\/\d+$/, "");
1980
- }
1981
- //#endregion
1982
- //#region src/diff/git.ts
1983
- function runGit$1(args, cwd, maxBuffer) {
1984
- const result = Bun.spawnSync(["git", ...args], {
1985
- cwd,
1986
- env: process.env,
1987
- maxBuffer,
1988
- stderr: "pipe",
1989
- stdout: "pipe"
1990
- });
1991
- if (result.exitCode !== 0) {
1992
- const failure = result.stderr?.toString().trim() || "unknown error";
1993
- throw new Error(`git ${args.join(" ")} failed: ${failure}`);
1994
- }
1995
- return result.stdout.toString();
1996
- }
1997
- //#endregion
1998
- //#region src/diff/diff.ts
1999
- const lockFilePattern = /(^|\/)(bun\.lock|package-lock\.json|pnpm-lock\.yaml|yarn\.lock|Cargo\.lock)$/;
2000
- const generatedPattern = /(^|\/)(dist|build|coverage|vendor)\//;
2001
- const maxInlineChangedLines = 1e3;
2002
- const maxCommentableRangeLines = 5e3;
2003
- function buildDiffManifest(options) {
2004
- const mergeBaseSha = runGit$1([
2005
- "merge-base",
2006
- options.baseSha,
2007
- options.headSha
2008
- ], options.cwd).trim();
2009
- const diffHead = options.includeWorkingTree ? void 0 : options.headSha;
2010
- const nameStatus = runGit$1(buildDiffArgs([
2011
- "--name-status",
2012
- "-z",
2013
- "--find-renames"
2014
- ], mergeBaseSha, diffHead), options.cwd);
2015
- const diffStats = getDiffStats(options.cwd, mergeBaseSha, diffHead);
2016
- const rawPatch = parseRawPatch(runGit$1(buildUnifiedDiffArgs(mergeBaseSha, diffHead, getPreExcludedFiles(diffStats)), options.cwd));
2017
- const files = parseNameStatus(nameStatus);
2018
- const parsedDiff = parseUnifiedDiff(rawPatch.patch, rawPatch.filePaths);
2019
- for (const file of files) {
2020
- const stats = diffStats.get(file.path);
2021
- file.additions = stats?.additions ?? 0;
2022
- file.deletions = stats?.deletions ?? 0;
2023
- const preExcludedReason = stats?.excludedReason;
2024
- const parsedFile = parsedDiff.get(file.path);
2025
- file.hunks = preExcludedReason ? [] : parsedFile?.hunks ?? [];
2026
- file.commentableRanges = preExcludedReason ? [] : parsedFile?.commentableRanges ?? [];
2027
- const excludedReason = preExcludedReason ?? getExcludedReason(file);
2028
- if (excludedReason) {
2029
- file.hunks = [];
2030
- file.commentableRanges = [];
2031
- file.excludedReason = excludedReason;
2032
- }
2033
- }
2034
- return parseDiffManifest({
2035
- baseSha: options.baseSha,
2036
- headSha: options.headSha,
2037
- mergeBaseSha,
2038
- files
2039
- });
2040
- }
2041
- function parseNameStatus(output) {
2042
- const fields = output.split("\0");
2043
- const files = [];
2044
- let index = 0;
2045
- while (index < fields.length) {
2046
- const rawStatus = fields[index++];
2047
- if (!rawStatus) continue;
2048
- const firstPath = fields[index++] ?? "";
2049
- const status = parseFileStatus(rawStatus);
2050
- if (status === "renamed") {
2051
- const secondPath = fields[index++] ?? "";
2052
- files.push(baseFile(secondPath || firstPath, status, firstPath));
2053
- continue;
2054
- }
2055
- files.push(baseFile(firstPath, status));
2056
- }
2057
- return files;
2058
- }
2059
- function parseUnifiedDiff(diff, filePaths) {
2060
- const state = createDiffParserState(filePaths);
2061
- for (const line of diff.split("\n")) parseUnifiedDiffLine(state, line);
2062
- finishActiveHunk(state);
2063
- return state.filesByPath;
2064
- }
2065
- function getDiffStats(cwd, baseSha, headSha) {
2066
- const output = runGit$1(buildDiffArgs([
2067
- "--numstat",
2068
- "-z",
2069
- "--find-renames"
2070
- ], baseSha, headSha), cwd);
2071
- const stats = /* @__PURE__ */ new Map();
2072
- for (const stat of parseNumstat(output)) {
2073
- if (stat.binary) {
2074
- stats.set(stat.path, {
2075
- additions: 0,
2076
- deletions: 0,
2077
- excludedReason: "binary diff"
2078
- });
2079
- continue;
2080
- }
2081
- stats.set(stat.path, {
2082
- additions: stat.additions,
2083
- deletions: stat.deletions,
2084
- excludedReason: stat.additions + stat.deletions > maxInlineChangedLines ? "oversized diff" : void 0
2085
- });
2086
- }
2087
- return stats;
2088
- }
2089
- function getPreExcludedFiles(stats) {
2090
- const excluded = /* @__PURE__ */ new Map();
2091
- for (const [filePath, stat] of stats) if (stat.excludedReason) excluded.set(filePath, stat.excludedReason);
2092
- return excluded;
2093
- }
2094
- function buildUnifiedDiffArgs(baseSha, headSha, excludedFiles) {
2095
- const args = buildDiffArgs([
2096
- "--raw",
2097
- "-z",
2098
- "--patch",
2099
- "--submodule=short",
2100
- "--unified=80",
2101
- "--find-renames"
2102
- ], baseSha, headSha);
2103
- if (excludedFiles.size === 0) return args;
2104
- return [
2105
- ...args,
2106
- "--",
2107
- ".",
2108
- ...[...excludedFiles.keys()].map((filePath) => `:(exclude,literal)${filePath}`)
2109
- ];
2110
- }
2111
- function buildDiffArgs(options, baseSha, headSha) {
2112
- return headSha ? [
2113
- "diff",
2114
- ...options,
2115
- baseSha,
2116
- headSha
2117
- ] : [
2118
- "diff",
2119
- ...options,
2120
- baseSha
2121
- ];
2122
- }
2123
- function parseNumstat(output) {
2124
- const fields = output.split("\0");
2125
- const stats = [];
2126
- let index = 0;
2127
- while (index < fields.length) {
2128
- const header = parseNumstatHeader(fields[index++] ?? "");
2129
- if (!header) continue;
2130
- const resolvedPath = resolveNumstatPath(header.path, fields, index);
2131
- index = resolvedPath.nextIndex;
2132
- const filePath = resolvedPath.path;
2133
- if (!filePath) continue;
2134
- const binary = header.rawAdditions === "-" || header.rawDeletions === "-";
2135
- stats.push({
2136
- path: filePath,
2137
- additions: binary ? 0 : Number(header.rawAdditions),
2138
- deletions: binary ? 0 : Number(header.rawDeletions),
2139
- binary
2140
- });
2141
- }
2142
- return stats;
1303
+ function parseNumstat(output) {
1304
+ const fields = output.split("\0");
1305
+ const stats = [];
1306
+ let index = 0;
1307
+ while (index < fields.length) {
1308
+ const header = parseNumstatHeader(fields[index++] ?? "");
1309
+ if (!header) continue;
1310
+ const resolvedPath = resolveNumstatPath(header.path, fields, index);
1311
+ index = resolvedPath.nextIndex;
1312
+ const filePath = resolvedPath.path;
1313
+ if (!filePath) continue;
1314
+ const binary = header.rawAdditions === "-" || header.rawDeletions === "-";
1315
+ stats.push({
1316
+ path: filePath,
1317
+ additions: binary ? 0 : Number(header.rawAdditions),
1318
+ deletions: binary ? 0 : Number(header.rawDeletions),
1319
+ binary
1320
+ });
1321
+ }
1322
+ return stats;
2143
1323
  }
2144
1324
  function resolveNumstatPath(path, fields, nextIndex) {
2145
1325
  if (path) return {
@@ -2202,11 +1382,15 @@ function parseFileStatus(rawStatus) {
2202
1382
  return "modified";
2203
1383
  }
2204
1384
  function getExcludedReason(file) {
1385
+ const pathExcludedReason = getPathExcludedReason(file);
1386
+ if (pathExcludedReason) return pathExcludedReason;
1387
+ if (file.additions + file.deletions > maxInlineChangedLines) return "oversized diff";
1388
+ if (commentableRangeLineCount(file.commentableRanges) > maxCommentableRangeLines) return "oversized diff";
1389
+ }
1390
+ function getPathExcludedReason(file) {
2205
1391
  if (file.status === "removed") return "removed file";
2206
1392
  if (lockFilePattern.test(file.path)) return "lock file";
2207
1393
  if (generatedPattern.test(file.path)) return "generated or build output";
2208
- if (file.additions + file.deletions > maxInlineChangedLines) return "oversized diff";
2209
- if (commentableRangeLineCount(file.commentableRanges) > maxCommentableRangeLines) return "oversized diff";
2210
1394
  }
2211
1395
  function commentableRangeLineCount(ranges) {
2212
1396
  return ranges.reduce((total, range) => total + range.endLine - range.startLine + 1, 0);
@@ -3693,7 +2877,7 @@ function createRuntimeLog(options) {
3693
2877
  },
3694
2878
  text(level, event, text) {
3695
2879
  if (level === "debug" && !debugEnabled) return;
3696
- emitRecord(sink, secrets, level, event, void 0, redact(text, secrets));
2880
+ emitRecord(sink, secrets, level, event, void 0, redact$1(text, secrets));
3697
2881
  },
3698
2882
  textSnippet(level, event, text, snippetOptions) {
3699
2883
  if (level === "debug" && !debugEnabled) return;
@@ -3703,7 +2887,7 @@ function createRuntimeLog(options) {
3703
2887
  return formatTextSnippet(text, secrets, snippetOptions);
3704
2888
  },
3705
2889
  async group(name, run) {
3706
- return await sink.group(redact(name, secrets), run);
2890
+ return await sink.group(redact$1(name, secrets), run);
3707
2891
  },
3708
2892
  addSecret(value) {
3709
2893
  addSecret(secrets, value);
@@ -3726,25 +2910,25 @@ function boundedLogSnippet(text, options) {
3726
2910
  function addSecret(secrets, value) {
3727
2911
  if (value && value.length >= 4) secrets.add(value);
3728
2912
  }
3729
- function redact(message, secrets) {
2913
+ function redact$1(message, secrets) {
3730
2914
  let redacted = message;
3731
2915
  for (const secret of secrets) redacted = redacted.split(secret).join("***");
3732
2916
  return redacted;
3733
2917
  }
3734
2918
  function compactFields(fields, secrets) {
3735
2919
  const compact = {};
3736
- for (const [key, value] of Object.entries(fields ?? {})) if (typeof value === "string") compact[key] = redact(value, secrets);
3737
- else if (Array.isArray(value)) compact[key] = value.map((item) => redact(item, secrets));
2920
+ for (const [key, value] of Object.entries(fields ?? {})) if (typeof value === "string") compact[key] = redact$1(value, secrets);
2921
+ else if (Array.isArray(value)) compact[key] = value.map((item) => redact$1(item, secrets));
3738
2922
  else if (value !== void 0) compact[key] = value;
3739
2923
  return compact;
3740
2924
  }
3741
2925
  function formatTextSnippet(text, secrets, options) {
3742
- return boundedLogSnippet(redact(text, secrets), options);
2926
+ return boundedLogSnippet(redact$1(text, secrets), options);
3743
2927
  }
3744
2928
  function emitRecord(sink, secrets, level, event, fields, text) {
3745
2929
  sink.log({
3746
2930
  level,
3747
- event: redact(event, secrets),
2931
+ event: redact$1(event, secrets),
3748
2932
  fields: compactFields(fields, secrets),
3749
2933
  text
3750
2934
  });
@@ -3837,6 +3021,146 @@ function findingFingerprint(finding) {
3837
3021
  return new Bun.CryptoHasher("sha256").update(basis).digest("hex").slice(0, 16);
3838
3022
  }
3839
3023
  //#endregion
3024
+ //#region src/review/agent/review-schema.ts
3025
+ const reviewFindingPropertyNames = [
3026
+ "body",
3027
+ "path",
3028
+ "rangeId",
3029
+ "side",
3030
+ "startLine",
3031
+ "endLine"
3032
+ ];
3033
+ const completeReviewFindingPropertyMask = (1 << reviewFindingPropertyNames.length) - 1;
3034
+ const unresolvedSchemaReference = Symbol("unresolvedSchemaReference");
3035
+ const singleSchemaKeywords = [
3036
+ "additionalItems",
3037
+ "additionalProperties",
3038
+ "contains",
3039
+ "else",
3040
+ "if",
3041
+ "items",
3042
+ "then",
3043
+ "unevaluatedProperties"
3044
+ ];
3045
+ const schemaArrayKeywords = [
3046
+ "allOf",
3047
+ "anyOf",
3048
+ "oneOf",
3049
+ "prefixItems"
3050
+ ];
3051
+ const schemaMapKeywords = [
3052
+ "dependentSchemas",
3053
+ "patternProperties",
3054
+ "properties"
3055
+ ];
3056
+ const alternativeSchemaKeywords = ["anyOf", "oneOf"];
3057
+ function schemaContainsReviewFinding(value) {
3058
+ if (!isRecord(value)) return false;
3059
+ return reachableSchemaContainsReviewFinding(value, value, /* @__PURE__ */ new Set());
3060
+ }
3061
+ function reachableSchemaContainsReviewFinding(schema, rootSchema, visited) {
3062
+ if (Array.isArray(schema)) return schema.some((child) => reachableSchemaContainsReviewFinding(child, rootSchema, visited));
3063
+ if (!isRecord(schema) || visited.has(schema)) return false;
3064
+ visited.add(schema);
3065
+ return schemaObjectPropertyMasks(schema, rootSchema, /* @__PURE__ */ new Set()).has(completeReviewFindingPropertyMask) || reachableChildSchemas(schema, rootSchema).some((child) => reachableSchemaContainsReviewFinding(child, rootSchema, visited));
3066
+ }
3067
+ function reachableChildSchemas(schema, rootSchema) {
3068
+ const referencedSchema = resolveLocalSchemaReference(rootSchema, schema.$ref);
3069
+ return [
3070
+ ...referencedSchema === void 0 ? [] : [referencedSchema],
3071
+ ...singleSchemaKeywords.map((keyword) => schema[keyword]),
3072
+ ...schemaArrayKeywords.flatMap((keyword) => Array.isArray(schema[keyword]) ? schema[keyword] : []),
3073
+ ...schemaMapKeywords.flatMap((keyword) => {
3074
+ const children = schema[keyword];
3075
+ return isRecord(children) ? Object.values(children) : [];
3076
+ })
3077
+ ];
3078
+ }
3079
+ function schemaObjectPropertyMasks(schema, rootSchema, visited) {
3080
+ if (schema === true || visited.has(schema)) return /* @__PURE__ */ new Set([0]);
3081
+ if (!isRecord(schema) || !schemaAllowsObject(schema, rootSchema, /* @__PURE__ */ new Set())) return /* @__PURE__ */ new Set();
3082
+ visited.add(schema);
3083
+ const conjunctiveMasks = conjunctiveSchemas(schema, rootSchema).reduce((masks, child) => combinePropertyMasks(masks, schemaObjectPropertyMasks(child, rootSchema, new Set(visited))), /* @__PURE__ */ new Set([reviewFindingPropertyMask(schema)]));
3084
+ return alternativeSchemaGroups(schema).reduce((masks, alternatives) => combinePropertyMasks(masks, alternativePropertyMasks(alternatives, rootSchema, visited)), conjunctiveMasks);
3085
+ }
3086
+ function conjunctiveSchemas(schema, rootSchema) {
3087
+ const referencedSchema = resolveLocalSchemaReference(rootSchema, schema.$ref);
3088
+ const allOf = Array.isArray(schema.allOf) ? schema.allOf : [];
3089
+ return referencedSchema === void 0 ? allOf : [referencedSchema, ...allOf];
3090
+ }
3091
+ function alternativeSchemaGroups(schema) {
3092
+ return alternativeSchemaKeywords.map((keyword) => schema[keyword]).filter((alternatives) => Array.isArray(alternatives));
3093
+ }
3094
+ function alternativePropertyMasks(alternatives, rootSchema, visited) {
3095
+ return new Set(alternatives.flatMap((child) => [...schemaObjectPropertyMasks(child, rootSchema, new Set(visited))]));
3096
+ }
3097
+ function reviewFindingPropertyMask(schema) {
3098
+ let mask = 0;
3099
+ const properties = isRecord(schema.properties) ? schema.properties : {};
3100
+ const patternProperties = isRecord(schema.patternProperties) ? schema.patternProperties : {};
3101
+ const propertyPatterns = Object.keys(patternProperties).flatMap((pattern) => {
3102
+ try {
3103
+ return [new RegExp(pattern)];
3104
+ } catch {
3105
+ return [];
3106
+ }
3107
+ });
3108
+ const required = new Set(Array.isArray(schema.required) ? schema.required.filter((propertyName) => typeof propertyName === "string") : []);
3109
+ for (const [index, propertyName] of reviewFindingPropertyNames.entries()) if (propertyName in properties || required.has(propertyName) || propertyPatterns.some((pattern) => pattern.test(propertyName))) mask |= 1 << index;
3110
+ return mask;
3111
+ }
3112
+ function combinePropertyMasks(left, right) {
3113
+ const combined = /* @__PURE__ */ new Set();
3114
+ for (const leftMask of left) for (const rightMask of right) {
3115
+ combined.add(leftMask | rightMask);
3116
+ if (combined.has(completeReviewFindingPropertyMask)) return /* @__PURE__ */ new Set([completeReviewFindingPropertyMask]);
3117
+ }
3118
+ return combined;
3119
+ }
3120
+ function schemaAllowsObject(schema, rootSchema, visited) {
3121
+ if (schema === false) return false;
3122
+ if (schema === true || !isRecord(schema) || visited.has(schema)) return true;
3123
+ visited.add(schema);
3124
+ return directSchemaAllowsObject(schema) && conjunctiveSchemas(schema, rootSchema).every((child) => schemaAllowsObject(child, rootSchema, new Set(visited))) && alternativeSchemaGroups(schema).every((alternatives) => alternatives.some((child) => schemaAllowsObject(child, rootSchema, new Set(visited))));
3125
+ }
3126
+ function directSchemaAllowsObject(schema) {
3127
+ const type = schema.type;
3128
+ if (typeof type === "string") return type === "object";
3129
+ if (Array.isArray(type) && !type.includes("object")) return false;
3130
+ if ("const" in schema && !isRecord(schema.const)) return false;
3131
+ return !Array.isArray(schema.enum) || schema.enum.some(isRecord);
3132
+ }
3133
+ function resolveLocalSchemaReference(rootSchema, reference) {
3134
+ if (reference === "#") return rootSchema;
3135
+ if (typeof reference !== "string" || !reference.startsWith("#/")) return;
3136
+ let current = rootSchema;
3137
+ for (const encodedSegment of reference.slice(2).split("/")) {
3138
+ const segment = decodePointerSegment(encodedSegment);
3139
+ if (segment === void 0) return;
3140
+ current = resolvePointerSegment(current, segment);
3141
+ if (current === unresolvedSchemaReference) return;
3142
+ }
3143
+ return current;
3144
+ }
3145
+ function decodePointerSegment(encodedSegment) {
3146
+ try {
3147
+ return decodeURIComponent(encodedSegment).replaceAll("~1", "/").replaceAll("~0", "~");
3148
+ } catch {
3149
+ return;
3150
+ }
3151
+ }
3152
+ function resolvePointerSegment(current, segment) {
3153
+ if (Array.isArray(current)) return resolveArrayPointerSegment(current, segment);
3154
+ if (!isRecord(current) || !(segment in current)) return unresolvedSchemaReference;
3155
+ return current[segment];
3156
+ }
3157
+ function resolveArrayPointerSegment(current, segment) {
3158
+ if (!/^(0|[1-9]\d*)$/.test(segment)) return unresolvedSchemaReference;
3159
+ const index = Number(segment);
3160
+ if (!Number.isSafeInteger(index) || index >= current.length) return unresolvedSchemaReference;
3161
+ return current[index];
3162
+ }
3163
+ //#endregion
3840
3164
  //#region src/review/agent/agent-prompt.ts
3841
3165
  async function renderAgentPrompt(options) {
3842
3166
  const prompt = await renderAgentDefinitionPrompt(options.agent, options.input, options.agentRunContext.prompt);
@@ -3893,12 +3217,12 @@ function outputPrompt(schema) {
3893
3217
  ]);
3894
3218
  if (schema.id === reviewResultSchemaId) {
3895
3219
  lines.splice(2, 0, `Example:\n${JSON.stringify(reviewSchemaExample$1(), null, 2)}`, ...suggestedFixRules);
3896
- lines.push("For inlineFindings, use only fields shown in the schema. Each finding's path, rangeId, and side must identify one Diff Manifest commentable range, and its startLine and endLine must select a valid span within that range. If no valid span applies, omit the finding.", ...inlineSelectionPromptLines(), `For inlineFindings.body, write the exact inline comment body. Use one short paragraph, at most two sentences, and at most 700 characters. Treat 700 as a hard ceiling, not a target; prefer 250-450 characters when possible.`);
3220
+ lines.push("For inlineFindings, use only fields shown in the schema. Each finding's path, rangeId, and side must identify one Diff Manifest commentable range, and its startLine and endLine must select a valid span within that range. If no valid span applies, omit the finding.", ...inlineSelectionPromptLines(), "For inlineFindings.body, write the exact inline comment body.");
3897
3221
  } else if (schemaMentionsField(schema.jsonSchema, "suggestedFix")) lines.splice(2, 0, ...suggestedFixRules);
3898
3222
  return lines.join("\n\n");
3899
3223
  }
3900
3224
  function customInlineSelectionPrompt(schema, diffManifest) {
3901
- if (!diffManifest || schema.id === reviewResultSchemaId) return;
3225
+ if (!diffManifest || schema.id === reviewResultSchemaId || !schemaContainsReviewFinding(schema.jsonSchema)) return;
3902
3226
  return promptSection("Inline Review Selection Policy", inlineSelectionPromptLines().join("\n"));
3903
3227
  }
3904
3228
  function inlineSelectionPromptLines() {
@@ -3922,16 +3246,16 @@ function schemaMentionsField(value, fieldName) {
3922
3246
  return Object.entries(value).some(([key, child]) => key === fieldName || schemaMentionsField(child, fieldName));
3923
3247
  }
3924
3248
  function reviewPolicyPrompt(schema) {
3925
- if (schema.id !== reviewResultSchemaId) return;
3249
+ if (schema.id !== reviewResultSchemaId && !schemaContainsReviewFinding(schema.jsonSchema)) return;
3926
3250
  return promptSection("Review Policy", [
3927
3251
  "Review only changed behavior.",
3928
3252
  "Report only actionable defects, security risks, regressions, or meaningful test gaps.",
3929
3253
  "Before emitting a finding, verify that the changed code introduces or exposes the issue, repository evidence supports it, and the impact is concrete. If any part is uncertain, omit it.",
3930
3254
  "When changed behavior crosses a function, type, API, configuration, or data boundary, inspect relevant callers, callees, and tests before deciding whether the change is defective or intentionally coordinated.",
3931
- "Put each actionable issue in inlineFindings. Do not leave actionable defects or test gaps only in the summary.",
3932
- "Base the summary only on changed behavior and evidence available in the Diff Manifest or read tools. Do not claim tests or checks ran, passed, or failed unless their output is present.",
3933
- "Inline finding bodies are final code-review comments, not analysis notes.",
3934
- `State the concrete defect and user-visible or runtime impact directly. Keep each body to one short paragraph, at most two sentences, and at most 700 characters.`,
3255
+ "Put each actionable issue in the schema's finding collection. Do not leave actionable defects or test gaps only in the summary.",
3256
+ "When the output includes a summary, base it only on changed behavior and evidence available in the Diff Manifest or read tools. Do not claim tests or checks ran, passed, or failed unless their output is present.",
3257
+ "Finding bodies must be publication-ready review prose, not analysis notes.",
3258
+ `State the concrete defect and user-visible or runtime impact directly. Keep each body to one short paragraph, at most two sentences, and at most 700 characters. Treat 700 as a hard ceiling, not a target; prefer 250-450 characters when possible.`,
3935
3259
  "Do not include step-by-step reasoning, broad context, praise, restated diff, alternatives, or code snippets unless they are necessary to identify the defect.",
3936
3260
  "Never copy a secret-looking literal from changed code into the review summary, inline finding body, or suggestedFix. Describe only the secret kind and location.",
3937
3261
  "Omit speculative, style-only, broad refactor, external-fact, and out-of-diff findings.",
@@ -3950,14 +3274,15 @@ function pathScopePrompt(paths) {
3950
3274
  ].join("\n");
3951
3275
  }
3952
3276
  function priorFindingsPrompt(state) {
3953
- const openFindings = state?.findings.filter((finding) => finding.status === "open") ?? [];
3954
- if (openFindings.length === 0) return;
3277
+ const findings = state?.findings.filter((finding) => finding.status === "open") ?? [];
3278
+ if (findings.length === 0) return;
3955
3279
  return [
3956
3280
  "Prior pipr findings:",
3957
3281
  JSON.stringify({
3958
3282
  reviewedHeadSha: state?.reviewedHeadSha,
3959
- findings: openFindings.map((finding) => ({
3283
+ findings: findings.map((finding) => ({
3960
3284
  id: finding.id,
3285
+ status: finding.status,
3961
3286
  path: finding.path,
3962
3287
  rangeId: finding.rangeId,
3963
3288
  side: finding.side,
@@ -4446,6 +3771,8 @@ const findingIdSchema = z.string().min(1).regex(/^[A-Za-z0-9_.-]+$/);
4446
3771
  const priorFindingStatusSchema = z.enum(["open", "resolved"]);
4447
3772
  const priorFindingRecordSchema = z.strictObject({
4448
3773
  id: findingIdSchema,
3774
+ anchorFingerprint: z.string().regex(/^[a-f0-9]{64}$/).optional(),
3775
+ issueFingerprint: z.string().regex(/^[a-f0-9]{64}$/).optional(),
4449
3776
  status: priorFindingStatusSchema,
4450
3777
  path: z.string().min(1),
4451
3778
  rangeId: z.string().min(1),
@@ -4469,33 +3796,23 @@ function buildPriorReviewState(options) {
4469
3796
  const nextFindings = /* @__PURE__ */ new Map();
4470
3797
  const usedPriorIds = /* @__PURE__ */ new Set();
4471
3798
  const stats = accumulateReviewStats(scopedPriorState?.stats, options.stats);
4472
- for (const finding of options.findings) {
4473
- const id = selectFindingId({
3799
+ const findings = options.findings.map((item) => item.finding);
3800
+ const fingerprintCounts = countFindingFingerprints(options.findings);
3801
+ for (const { finding, anchorFingerprint, issueFingerprint, previousPath } of options.findings) {
3802
+ const record = buildFindingRecord({
4474
3803
  finding,
4475
- findings: options.findings,
3804
+ findings,
4476
3805
  priorFindings,
4477
- usedPriorIds
3806
+ usedPriorIds,
3807
+ reviewedHeadSha: options.reviewedHeadSha,
3808
+ anchorFingerprint,
3809
+ issueFingerprint,
3810
+ previousPath,
3811
+ fingerprintCounts
4478
3812
  });
4479
- const prior = priorFindings.get(id);
4480
- if (prior) usedPriorIds.add(prior.id);
4481
- const record = {
4482
- id,
4483
- status: "open",
4484
- path: finding.path,
4485
- rangeId: finding.rangeId,
4486
- side: finding.side,
4487
- startLine: finding.startLine,
4488
- endLine: finding.endLine,
4489
- firstSeenHeadSha: prior?.firstSeenHeadSha ?? options.reviewedHeadSha,
4490
- lastSeenHeadSha: options.reviewedHeadSha,
4491
- ...prior?.lastCommentedHeadSha ? { lastCommentedHeadSha: prior.lastCommentedHeadSha } : {}
4492
- };
4493
- nextFindings.set(id, record);
4494
- }
4495
- for (const prior of priorFindings.values()) {
4496
- if (nextFindings.has(prior.id)) continue;
4497
- nextFindings.set(prior.id, prior);
3813
+ nextFindings.set(record.id, record);
4498
3814
  }
3815
+ addHistoricalFindings(nextFindings, priorFindings.values());
4499
3816
  return {
4500
3817
  version: 1,
4501
3818
  reviewedHeadSha: options.reviewedHeadSha,
@@ -4504,6 +3821,57 @@ function buildPriorReviewState(options) {
4504
3821
  ...stats ? { stats } : {}
4505
3822
  };
4506
3823
  }
3824
+ function buildFindingRecord(options) {
3825
+ const selection = selectPriorFindingRecord(options);
3826
+ const prior = options.priorFindings.get(selection.id);
3827
+ markPriorFindingUsed(options.usedPriorIds, prior);
3828
+ return {
3829
+ id: selection.id,
3830
+ ...findingIdentity(options.anchorFingerprint, options.issueFingerprint),
3831
+ status: selection.status,
3832
+ path: options.finding.path,
3833
+ rangeId: options.finding.rangeId,
3834
+ side: options.finding.side,
3835
+ startLine: options.finding.startLine,
3836
+ endLine: options.finding.endLine,
3837
+ ...findingHistory(prior, options.reviewedHeadSha)
3838
+ };
3839
+ }
3840
+ function selectPriorFindingRecord(options) {
3841
+ const resolvedMatch = matchResolvedFindingRecord([...options.priorFindings.values()], options.finding, options.anchorFingerprint, options.issueFingerprint, options.fingerprintCounts, options.previousPath);
3842
+ if (resolvedMatch) return {
3843
+ id: resolvedMatch.id,
3844
+ status: "resolved"
3845
+ };
3846
+ return {
3847
+ id: selectFindingId({
3848
+ finding: options.finding,
3849
+ findings: options.findings,
3850
+ priorFindings: options.priorFindings,
3851
+ usedPriorIds: options.usedPriorIds
3852
+ }),
3853
+ status: "open"
3854
+ };
3855
+ }
3856
+ function markPriorFindingUsed(usedPriorIds, prior) {
3857
+ if (prior) usedPriorIds.add(prior.id);
3858
+ }
3859
+ function findingIdentity(anchorFingerprint, issueFingerprint) {
3860
+ return {
3861
+ ...anchorFingerprint ? { anchorFingerprint } : {},
3862
+ ...issueFingerprint ? { issueFingerprint } : {}
3863
+ };
3864
+ }
3865
+ function findingHistory(prior, reviewedHeadSha) {
3866
+ return {
3867
+ firstSeenHeadSha: prior?.firstSeenHeadSha ?? reviewedHeadSha,
3868
+ lastSeenHeadSha: reviewedHeadSha,
3869
+ ...prior?.lastCommentedHeadSha ? { lastCommentedHeadSha: prior.lastCommentedHeadSha } : {}
3870
+ };
3871
+ }
3872
+ function addHistoricalFindings(findings, historicalFindings) {
3873
+ for (const finding of historicalFindings) if (!findings.has(finding.id)) findings.set(finding.id, finding);
3874
+ }
4507
3875
  function resolvePriorFindings(state, findingIds) {
4508
3876
  const resolved = new Set(findingIds);
4509
3877
  return {
@@ -4523,6 +3891,22 @@ function matchFindingRecord(state, finding) {
4523
3891
  if (deterministic) return deterministic;
4524
3892
  return findOpenOverlappingFinding(state.findings, finding);
4525
3893
  }
3894
+ function matchResolvedFindingRecord(records, finding, anchorFingerprint, issueFingerprint, currentFingerprintCounts, previousPath) {
3895
+ if (!anchorFingerprint || !issueFingerprint || (currentFingerprintCounts?.get(findingFingerprintKey(finding.path, anchorFingerprint, issueFingerprint)) ?? 1) !== 1) return;
3896
+ const candidates = records.filter((record) => record.anchorFingerprint === anchorFingerprint && record.issueFingerprint === issueFingerprint && (record.path === finding.path || record.path === previousPath));
3897
+ return candidates.length === 1 && candidates[0]?.status === "resolved" ? candidates[0] : void 0;
3898
+ }
3899
+ function countFindingFingerprints(findings) {
3900
+ const counts = /* @__PURE__ */ new Map();
3901
+ for (const { finding, anchorFingerprint, issueFingerprint } of findings) if (anchorFingerprint && issueFingerprint) {
3902
+ const key = findingFingerprintKey(finding.path, anchorFingerprint, issueFingerprint);
3903
+ counts.set(key, (counts.get(key) ?? 0) + 1);
3904
+ }
3905
+ return counts;
3906
+ }
3907
+ function findingFingerprintKey(path, anchorFingerprint, issueFingerprint) {
3908
+ return `${path}\0${anchorFingerprint}\0${issueFingerprint}`;
3909
+ }
4526
3910
  function renderMainCommentMarker(options) {
4527
3911
  return `<!-- ${options.marker} change=${options.changeNumber} version=1 state=${encodeReviewState({
4528
3912
  ...options.reviewState,
@@ -4587,6 +3971,20 @@ function applyResolvedFindingMarkers(state, commentBodies) {
4587
3971
  }))
4588
3972
  };
4589
3973
  }
3974
+ function applyNativeThreadResolutions(state, resolutions) {
3975
+ const resolutionByFinding = new Map(resolutions.map((resolution) => [`${resolution.findingId}:${resolution.findingHeadSha}`, resolution.resolved]));
3976
+ return {
3977
+ ...state,
3978
+ findings: state.findings.map((finding) => {
3979
+ if (!finding.lastCommentedHeadSha) return finding;
3980
+ const resolved = resolutionByFinding.get(`${finding.id}:${finding.lastCommentedHeadSha}`);
3981
+ return resolved === void 0 ? finding : {
3982
+ ...finding,
3983
+ status: resolved ? "resolved" : "open"
3984
+ };
3985
+ })
3986
+ };
3987
+ }
4590
3988
  function extractVerifierResponseMarkers(commentBodies) {
4591
3989
  return new Set(extractMarkerRecords(commentBodies, verifierResponseMarkerPrefix).map((record) => record.marker));
4592
3990
  }
@@ -4782,7 +4180,7 @@ function publicationPlanForHostCapabilities(plan, capabilities) {
4782
4180
  }
4783
4181
  function buildPublicationPlan(options) {
4784
4182
  const reviewState = options.reviewState ?? buildPriorReviewState({
4785
- findings: options.inlineItems.map((item) => item.finding),
4183
+ findings: options.inlineItems.map((item) => ({ finding: item.finding })),
4786
4184
  reviewedHeadSha: options.metadata.reviewedHeadSha,
4787
4185
  selectedTasks: options.metadata.selectedTasks
4788
4186
  });
@@ -4827,10 +4225,12 @@ function preparePublishableInlineFindings(options) {
4827
4225
  }
4828
4226
  function prepareInlinePublicationItemsForPublishableFindings(options) {
4829
4227
  const seenFindingIds = /* @__PURE__ */ new Set();
4830
- return inlinePublicationItemsSchema.parse(options.publishableFindings.flatMap(({ finding: publishableFinding, range, previousPath }) => {
4228
+ const fingerprintCounts = countFindingFingerprints(options.publishableFindings);
4229
+ return inlinePublicationItemsSchema.parse(options.publishableFindings.flatMap(({ finding: publishableFinding, range, previousPath, anchorFingerprint, issueFingerprint }) => {
4831
4230
  const findingId = findingIdFor(publishableFinding, options.reviewState);
4832
4231
  const stateRecord = options.reviewState ? matchFindingRecord(options.reviewState, publishableFinding) : void 0;
4833
- if (seenFindingIds.has(findingId) || stateRecord?.lastCommentedHeadSha === options.reviewedHeadSha) return [];
4232
+ const resolvedRecord = options.reviewState ? matchResolvedFindingRecord(options.reviewState.findings, publishableFinding, anchorFingerprint, issueFingerprint, fingerprintCounts, previousPath) : void 0;
4233
+ if (seenFindingIds.has(findingId) || resolvedRecord !== void 0 || stateRecord?.lastCommentedHeadSha === options.reviewedHeadSha) return [];
4834
4234
  seenFindingIds.add(findingId);
4835
4235
  const marker = inlineFindingMarker(findingId, options.reviewedHeadSha);
4836
4236
  return [inlinePublicationItemSchema.parse({
@@ -4977,10 +4377,17 @@ function buildCommentPublishingPlan(options) {
4977
4377
  const publishableInlineFindings = preparePublishableInlineFindings({
4978
4378
  validated: options.validated,
4979
4379
  manifest: options.manifest
4380
+ }).map((item) => {
4381
+ const fingerprint = selectedCodeFingerprint(item.finding, item.range);
4382
+ return fingerprint ? {
4383
+ ...item,
4384
+ anchorFingerprint: fingerprint,
4385
+ issueFingerprint: findingIssueFingerprint(item.finding)
4386
+ } : item;
4980
4387
  });
4981
4388
  const reviewState = buildPriorReviewState({
4982
4389
  priorState: options.priorReviewState,
4983
- findings: publishableInlineFindings.map((item) => item.finding),
4390
+ findings: publishableInlineFindings,
4984
4391
  reviewedHeadSha: options.event.change.head.sha,
4985
4392
  selectedTasks: options.metadata.selectedTasks,
4986
4393
  stats: options.metadata.stats
@@ -5011,6 +4418,20 @@ function buildCommentPublishingPlan(options) {
5011
4418
  inlineCommentDrafts: publicationPlan.inlineItems
5012
4419
  };
5013
4420
  }
4421
+ function selectedCodeFingerprint(finding, range) {
4422
+ if (range.preview === void 0) return;
4423
+ const lines = range.preview.replaceAll("\r\n", "\n").replaceAll("\r", "\n").split("\n");
4424
+ const startOffset = finding.startLine - range.startLine;
4425
+ const endOffset = finding.endLine - range.startLine + 1;
4426
+ if (startOffset < 0 || endOffset > lines.length) return;
4427
+ const selected = lines.slice(startOffset, endOffset).map((line) => line.trimEnd()).join("\n");
4428
+ return new Bun.CryptoHasher("sha256").update(selected).digest("hex");
4429
+ }
4430
+ function findingIssueFingerprint(finding) {
4431
+ const identity = finding.body.normalize("NFKC").toLowerCase().replace(/<[^>]*>/g, " ").replace(/\[([^\]]+)\]\([^)]*\)/g, "$1").replace(/[^\p{L}\p{N}_]+/gu, " ").trim().replace(/\s+/g, " ") || finding.body.normalize("NFKC").toLowerCase().trim();
4432
+ if (!identity) return;
4433
+ return new Bun.CryptoHasher("sha256").update(identity).digest("hex");
4434
+ }
5014
4435
  //#endregion
5015
4436
  //#region src/review/publication-redaction.ts
5016
4437
  function redactReviewPublication(options) {
@@ -5700,344 +5121,755 @@ async function runTaskRuntime(options) {
5700
5121
  durationMs: Date.now() - started,
5701
5122
  error: error instanceof Error ? error.message : String(error)
5702
5123
  });
5703
- if (options.log?.debugEnabled && error instanceof Error && error.stack) options.log.text("debug", "error stack", error.stack);
5704
- return {
5705
- taskName: task.name,
5706
- output: {
5707
- ...output,
5708
- check
5709
- },
5710
- error
5711
- };
5712
- }
5713
- }));
5714
- const taskChecks = taskResults.map((result) => runtimeTaskCheckResult(result.taskName, result.output.check ?? { conclusion: "success" }));
5715
- const failedTask = taskResults.find((result) => result.error !== void 0);
5716
- if (failedTask) {
5717
- publishFailedRunTaskChecks(options, taskChecks);
5718
- throw failedTask.error instanceof Error ? failedTask.error : new Error(String(failedTask.error));
5719
- }
5720
- const output = mergeTaskOutputs(taskResults);
5721
- options.log?.info("task runtime collected", {
5722
- findings: output.findings.length,
5723
- providerModels: output.providerModels,
5724
- repairAttempted: output.repairAttempted
5725
- });
5726
- const commandResponse = commandResponseResultFromOutput({
5727
- provider,
5728
- diffManifest,
5729
- output,
5730
- taskChecks,
5731
- commandInvocation: options.commandInvocation,
5732
- secretRedactor: options.secretRedactor
5733
- });
5734
- if (commandResponse) {
5735
- publishTaskChecks(options.checkSink, commandResponse.taskChecks);
5736
- return commandResponse;
5737
- }
5738
- assertReviewCommentOutput(output, options.commandInvocation !== void 0);
5739
- const main = typeof output.comment.value === "string" ? output.comment.value : output.comment.value.main ?? "Review completed.";
5740
- const validated = validateReviewResult(collectedReview(output, main), diffManifest, {
5741
- expectedHeadSha: options.event.change.head.sha,
5742
- pathScopeForFinding: (_finding, index) => output.findings[index]?.paths
5743
- });
5744
- const verifier = await runSynchronizeVerifier({
5745
- options,
5746
- config,
5747
- provider,
5748
- diffManifest,
5749
- priorReviewState,
5750
- runId,
5751
- piRunSink: runtimeOptions.piRunSink
5752
- });
5753
- const stats = reviewStatsForRuns(piRuns, Date.now() - runtimeStarted);
5754
- const redactedPublication = redactReviewPublication({
5755
- main,
5756
- validated,
5757
- threadActions: verifier.threadActions,
5758
- taskChecks,
5124
+ if (options.log?.debugEnabled && error instanceof Error && error.stack) options.log.text("debug", "error stack", error.stack);
5125
+ return {
5126
+ taskName: task.name,
5127
+ output: {
5128
+ ...output,
5129
+ check
5130
+ },
5131
+ error
5132
+ };
5133
+ }
5134
+ }));
5135
+ const taskChecks = taskResults.map((result) => runtimeTaskCheckResult(result.taskName, result.output.check ?? { conclusion: "success" }));
5136
+ const failedTask = taskResults.find((result) => result.error !== void 0);
5137
+ if (failedTask) {
5138
+ publishFailedRunTaskChecks(options, taskChecks);
5139
+ throw failedTask.error instanceof Error ? failedTask.error : new Error(String(failedTask.error));
5140
+ }
5141
+ const output = mergeTaskOutputs(taskResults);
5142
+ options.log?.info("task runtime collected", {
5143
+ findings: output.findings.length,
5144
+ providerModels: output.providerModels,
5145
+ repairAttempted: output.repairAttempted
5146
+ });
5147
+ const commandResponse = commandResponseResultFromOutput({
5148
+ provider,
5149
+ diffManifest,
5150
+ output,
5151
+ taskChecks,
5152
+ commandInvocation: options.commandInvocation,
5153
+ secretRedactor: options.secretRedactor
5154
+ });
5155
+ if (commandResponse) {
5156
+ publishTaskChecks(options.checkSink, commandResponse.taskChecks);
5157
+ return commandResponse;
5158
+ }
5159
+ assertReviewCommentOutput(output, options.commandInvocation !== void 0);
5160
+ const main = typeof output.comment.value === "string" ? output.comment.value : output.comment.value.main ?? "Review completed.";
5161
+ const validated = validateReviewResult(collectedReview(output, main), diffManifest, {
5162
+ expectedHeadSha: options.event.change.head.sha,
5163
+ pathScopeForFinding: (_finding, index) => output.findings[index]?.paths
5164
+ });
5165
+ const verifier = await runSynchronizeVerifier({
5166
+ options,
5167
+ config,
5168
+ provider,
5169
+ diffManifest,
5170
+ priorReviewState,
5171
+ runId,
5172
+ piRunSink: runtimeOptions.piRunSink
5173
+ });
5174
+ const stats = reviewStatsForRuns(piRuns, Date.now() - runtimeStarted);
5175
+ const redactedPublication = redactReviewPublication({
5176
+ main,
5177
+ validated,
5178
+ threadActions: verifier.threadActions,
5179
+ taskChecks,
5180
+ redactor: options.secretRedactor
5181
+ });
5182
+ const publishing = buildCommentPublishingPlan({
5183
+ event: options.event,
5184
+ main: redactedPublication.main,
5185
+ validated: redactedPublication.validated,
5186
+ manifest: diffManifest,
5187
+ maxInlineComments: config.publication.maxInlineComments,
5188
+ maxStoredFindings: config.publication.maxStoredFindings,
5189
+ showHeader: config.publication.showHeader,
5190
+ showFooter: config.publication.showFooter,
5191
+ showStats: config.publication.showStats,
5192
+ priorReviewState: verifier.priorReviewState,
5193
+ threadActions: redactedPublication.threadActions,
5194
+ metadata: {
5195
+ runtimeVersion,
5196
+ configVersion: options.versionCompatibility?.configVersion,
5197
+ trustedConfigSha: options.trustedConfigSha,
5198
+ trustedConfigHash: options.trustedConfigHash,
5199
+ reviewedHeadSha: options.event.change.head.sha,
5200
+ providerModels: output.providerModels.length + verifier.providerModels.length > 0 ? uniq([...output.providerModels, ...verifier.providerModels]) : [provider.model],
5201
+ selectedTasks,
5202
+ failedTasks: [],
5203
+ validFindings: validated.validFindings.length,
5204
+ droppedFindings: validated.droppedFindings.length,
5205
+ ...stats ? { stats } : {}
5206
+ }
5207
+ });
5208
+ const publicationPlan = publishing.publicationPlan;
5209
+ publishTaskChecks(options.checkSink, redactedPublication.taskChecks);
5210
+ options.log?.info("review validated", {
5211
+ validFindings: validated.validFindings.length,
5212
+ droppedFindings: validated.droppedFindings.length,
5213
+ inlineDrafts: publishing.inlineCommentDrafts.length,
5214
+ threadActions: verifier.threadActions.length
5215
+ });
5216
+ return {
5217
+ kind: "review",
5218
+ provider,
5219
+ diffManifest,
5220
+ review: redactedPublication.validated.review,
5221
+ validated: redactedPublication.validated,
5222
+ publicationPlan,
5223
+ mainComment: publicationPlan.mainComment,
5224
+ inlineCommentDrafts: publishing.inlineCommentDrafts,
5225
+ taskChecks: redactedPublication.taskChecks,
5226
+ repairAttempted: output.repairAttempted
5227
+ };
5228
+ }
5229
+ function publishFailedRunTaskChecks(options, taskChecks) {
5230
+ const redacted = redactCommandPublication({
5231
+ body: "",
5232
+ taskChecks,
5233
+ redactor: options.secretRedactor
5234
+ });
5235
+ publishTaskChecks(options.checkSink, redacted.taskChecks);
5236
+ }
5237
+ function commandResponseResultFromOutput(options) {
5238
+ const commandResponse = options.output.commandResponse;
5239
+ if (!commandResponse) return;
5240
+ if (!options.commandInvocation) throw new Error("ctx.command.reply(...) is only available for command-triggered tasks");
5241
+ return commandResponseRuntimeResult({
5242
+ ...options,
5243
+ commandResponse,
5244
+ commandInvocation: options.commandInvocation
5245
+ });
5246
+ }
5247
+ function assertReviewCommentOutput(output, hasCommandInvocation) {
5248
+ if (output.comment) return;
5249
+ throw new Error(hasCommandInvocation ? "ctx.comment(...) or ctx.command.reply(...) must be called exactly once per selected run" : "ctx.comment(...) must be called exactly once per selected run");
5250
+ }
5251
+ async function runSynchronizeVerifier(options) {
5252
+ if (options.options.event.rawAction !== "synchronize") return {
5253
+ priorReviewState: options.priorReviewState,
5254
+ threadActions: [],
5255
+ providerModels: []
5256
+ };
5257
+ const config = options.config;
5258
+ return await runInternalVerifier({
5259
+ workspace: options.options.workspace,
5260
+ config,
5261
+ event: options.options.event,
5262
+ provider: options.provider,
5263
+ verifierProvider: resolveProvider(config, config.publication.autoResolve.model ?? config.defaultProvider),
5264
+ plan: options.options.plan,
5265
+ env: options.options.env,
5266
+ piExecutable: options.options.piExecutable,
5267
+ piRunner: options.options.piRunner,
5268
+ log: options.options.log,
5269
+ diffManifest: options.diffManifest,
5270
+ priorReviewState: options.priorReviewState,
5271
+ threadContexts: await options.options.loadInlineThreadContexts?.() ?? [],
5272
+ mode: { kind: "synchronize" },
5273
+ runId: options.runId,
5274
+ piRunSink: options.piRunSink
5275
+ });
5276
+ }
5277
+ function createTaskContext(options) {
5278
+ const repositorySlugParts = options.event.repository.slug.split("/");
5279
+ let taskContext;
5280
+ taskContext = {
5281
+ run: { id: options.runId },
5282
+ repository: {
5283
+ root: options.workspace,
5284
+ owner: repositorySlugParts.length > 1 ? repositorySlugParts[0] : void 0,
5285
+ name: repositorySlugParts.at(-1) ?? "repo"
5286
+ },
5287
+ change: {
5288
+ number: options.event.change.number,
5289
+ title: options.event.change.title,
5290
+ description: options.event.change.description,
5291
+ url: options.event.change.url,
5292
+ author: options.event.change.author,
5293
+ base: options.event.change.base,
5294
+ head: options.event.change.head,
5295
+ isFork: options.event.change.isFork,
5296
+ async diffManifest(manifestOptions) {
5297
+ const key = JSON.stringify(manifestOptions ?? {});
5298
+ const cached = options.manifestCache.get(key);
5299
+ if (cached) return cloneDiffManifest(cached);
5300
+ const manifest = projectDiffManifest(options.diffManifest, manifestOptions);
5301
+ options.manifestCache.set(key, manifest);
5302
+ return cloneDiffManifest(manifest);
5303
+ },
5304
+ async changedFiles() {
5305
+ return options.diffManifest.files.map((file) => ({
5306
+ path: file.path,
5307
+ previousPath: file.previousPath,
5308
+ status: file.status
5309
+ }));
5310
+ },
5311
+ async currentHeadSha() {
5312
+ return options.event.change.head.sha;
5313
+ }
5314
+ },
5315
+ platform: { id: options.event.platform.id },
5316
+ command: options.commandInvocation ? {
5317
+ name: options.commandInvocation.name,
5318
+ line: options.commandInvocation.line,
5319
+ arguments: { ...options.commandInvocation.arguments },
5320
+ async reply(markdown) {
5321
+ collectCommandResponse(options.output, markdown, options.taskName);
5322
+ }
5323
+ } : void 0,
5324
+ secret(secret) {
5325
+ return resolveTaskSecret(secret, options);
5326
+ },
5327
+ pi: { async run(agent, input, runOptions) {
5328
+ const result = await runReviewAgent({
5329
+ agent,
5330
+ input,
5331
+ runOptions,
5332
+ runtime: {
5333
+ ...options,
5334
+ taskContext,
5335
+ runId: options.runId,
5336
+ piRunSink: options.piRunSink
5337
+ }
5338
+ });
5339
+ options.output.providerModels.push(...result.providerModels);
5340
+ if (result.repairAttempted) options.output.repairAttempted = true;
5341
+ trackResultFindingScope(options.output, result.value, runOptions?.paths);
5342
+ return agentOutputForTaskContext(agent, result.value);
5343
+ } },
5344
+ review: { async prior() {
5345
+ return priorReviewForTask(options.priorMainComment, options.priorReviewState);
5346
+ } },
5347
+ check: createCheckHandle(options.output),
5348
+ async comment(value) {
5349
+ collectComment(options.output, value, options.taskName);
5350
+ },
5351
+ log: options.taskLog ?? console
5352
+ };
5353
+ return taskContext;
5354
+ }
5355
+ function agentOutputForTaskContext(_agent, value) {
5356
+ return value;
5357
+ }
5358
+ function resolveTaskSecret(secret, options) {
5359
+ if (secret.kind !== "pipr.secret" || typeof secret.name !== "string") throw new Error("ctx.secret(...) requires a pipr.secret reference");
5360
+ const value = (options.env ?? process.env)[secret.name];
5361
+ if (!value) throw new Error(`Missing secret env var: ${secret.name}`);
5362
+ options.log?.addSecret(value);
5363
+ options.secretRedactor?.addSecret(value);
5364
+ return value;
5365
+ }
5366
+ function commandResponseRuntimeResult(options) {
5367
+ const redacted = redactCommandPublication({
5368
+ body: options.commandResponse.value,
5369
+ taskChecks: options.taskChecks,
5759
5370
  redactor: options.secretRedactor
5760
5371
  });
5761
- const publishing = buildCommentPublishingPlan({
5372
+ return {
5373
+ kind: "command-response",
5374
+ provider: options.provider,
5375
+ diffManifest: options.diffManifest,
5376
+ taskChecks: redacted.taskChecks,
5377
+ repairAttempted: options.output.repairAttempted,
5378
+ commandResponse: {
5379
+ commandName: options.commandInvocation.name,
5380
+ line: options.commandInvocation.line,
5381
+ arguments: options.commandInvocation.arguments,
5382
+ body: redacted.body
5383
+ }
5384
+ };
5385
+ }
5386
+ function publishTaskChecks(sink, checks) {
5387
+ for (const check of checks) sink?.setTaskResult(check);
5388
+ }
5389
+ function skippedTaskRuntimeResult(options) {
5390
+ const reason = options.reason ?? (options.taskName ? `Task '${options.taskName}' was not registered` : "No tasks matched the change request event");
5391
+ const review = {
5392
+ summary: { body: reason },
5393
+ inlineFindings: []
5394
+ };
5395
+ const validated = {
5396
+ review,
5397
+ validFindings: [],
5398
+ droppedFindings: []
5399
+ };
5400
+ const publicationPlan = buildCommentPublishingPlan({
5762
5401
  event: options.event,
5763
- main: redactedPublication.main,
5764
- validated: redactedPublication.validated,
5765
- manifest: diffManifest,
5766
- maxInlineComments: config.publication.maxInlineComments,
5767
- maxStoredFindings: config.publication.maxStoredFindings,
5768
- showHeader: config.publication.showHeader,
5769
- showFooter: config.publication.showFooter,
5770
- showStats: config.publication.showStats,
5771
- priorReviewState: verifier.priorReviewState,
5772
- threadActions: redactedPublication.threadActions,
5402
+ main: reason,
5403
+ validated,
5404
+ manifest: options.diffManifest,
5405
+ maxInlineComments: options.config.publication.maxInlineComments,
5406
+ maxStoredFindings: options.config.publication.maxStoredFindings,
5407
+ showHeader: options.config.publication.showHeader,
5408
+ showFooter: options.config.publication.showFooter,
5409
+ showStats: options.config.publication.showStats,
5773
5410
  metadata: {
5774
5411
  runtimeVersion,
5775
5412
  configVersion: options.versionCompatibility?.configVersion,
5776
5413
  trustedConfigSha: options.trustedConfigSha,
5777
5414
  trustedConfigHash: options.trustedConfigHash,
5778
5415
  reviewedHeadSha: options.event.change.head.sha,
5779
- providerModels: output.providerModels.length + verifier.providerModels.length > 0 ? uniq([...output.providerModels, ...verifier.providerModels]) : [provider.model],
5780
- selectedTasks,
5416
+ providerModels: [options.provider.model],
5417
+ selectedTasks: [],
5781
5418
  failedTasks: [],
5782
- validFindings: validated.validFindings.length,
5783
- droppedFindings: validated.droppedFindings.length,
5784
- ...stats ? { stats } : {}
5419
+ validFindings: 0,
5420
+ droppedFindings: 0
5785
5421
  }
5786
- });
5787
- const publicationPlan = publishing.publicationPlan;
5788
- publishTaskChecks(options.checkSink, redactedPublication.taskChecks);
5789
- options.log?.info("review validated", {
5790
- validFindings: validated.validFindings.length,
5791
- droppedFindings: validated.droppedFindings.length,
5792
- inlineDrafts: publishing.inlineCommentDrafts.length,
5793
- threadActions: verifier.threadActions.length
5794
- });
5422
+ }).publicationPlan;
5423
+ return {
5424
+ kind: "skipped",
5425
+ skipReason: reason,
5426
+ provider: options.provider,
5427
+ diffManifest: options.diffManifest,
5428
+ review,
5429
+ validated,
5430
+ publicationPlan,
5431
+ mainComment: publicationPlan.mainComment,
5432
+ inlineCommentDrafts: [],
5433
+ taskChecks: [],
5434
+ repairAttempted: false
5435
+ };
5436
+ }
5437
+ //#endregion
5438
+ //#region src/shared/secret-redactor-core.ts
5439
+ const redactedSecret = "[redacted secret]";
5440
+ const minimumSecretLength = 4;
5441
+ function createSecretRedactor(initialSecrets) {
5442
+ const secrets = new Set(initialSecrets.filter((value) => value.length >= minimumSecretLength));
5443
+ return {
5444
+ addSecret(value) {
5445
+ if (value && value.length >= minimumSecretLength) secrets.add(value);
5446
+ },
5447
+ redact(value) {
5448
+ const ordered = [...secrets].sort((left, right) => right.length - left.length);
5449
+ let next = value;
5450
+ for (const secret of ordered) next = next.replaceAll(secret, redactedSecret);
5451
+ return {
5452
+ value: next,
5453
+ detected: next !== value
5454
+ };
5455
+ }
5456
+ };
5457
+ }
5458
+ //#endregion
5459
+ //#region src/shared/secret-redactor.ts
5460
+ function createKnownSecretRedactor(options) {
5461
+ return createSecretRedactor(sensitiveEnvironmentValues(options?.env ?? process.env));
5462
+ }
5463
+ //#endregion
5464
+ //#region src/hosts/http.ts
5465
+ var CodeHostHttpError = class extends Error {
5466
+ status;
5467
+ constructor(message, status) {
5468
+ super(message);
5469
+ this.name = "CodeHostHttpError";
5470
+ this.status = status;
5471
+ }
5472
+ };
5473
+ function createCodeHostHttpClient(options) {
5474
+ const fetchRequest = options.fetch ?? fetch;
5475
+ const sleep = options.sleep ?? ((milliseconds) => Bun.sleep(milliseconds));
5476
+ const successThrottle = options.successThrottle ?? createCodeHostSuccessThrottle(sleep);
5477
+ const maxRetries = options.maxRetries ?? 2;
5478
+ const requestTimeoutMilliseconds = options.requestTimeoutMilliseconds ?? 3e4;
5479
+ const secrets = [...new Headers(options.headers).values()].flatMap((value) => [value, value.split(/\s+/).at(-1)].filter((candidate) => candidate !== void 0 && candidate.length >= 8));
5480
+ return { async json(path, schema, init = {}) {
5481
+ const method = init.method?.toUpperCase() ?? "GET";
5482
+ for (let attempt = 0;; attempt += 1) {
5483
+ await successThrottle.wait();
5484
+ const timeoutSignal = AbortSignal.timeout(requestTimeoutMilliseconds);
5485
+ const response = await fetchRequest(new URL(path, options.baseUrl), {
5486
+ ...init,
5487
+ headers: {
5488
+ ...Object.fromEntries(new Headers(options.headers)),
5489
+ ...Object.fromEntries(new Headers(init.headers))
5490
+ },
5491
+ signal: init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal
5492
+ });
5493
+ const retryAfter = retryAfterMilliseconds(response.headers.get("Retry-After"));
5494
+ if (response.ok) {
5495
+ successThrottle.update(response.headers.get("Retry-After"));
5496
+ return schema.parse(await response.json());
5497
+ }
5498
+ if (shouldRetryCodeHostRequest({
5499
+ method,
5500
+ attempt,
5501
+ maxRetries,
5502
+ response,
5503
+ retryStatuses: options.retryNonIdempotentStatuses
5504
+ })) {
5505
+ await sleep(retryAfter || 250 * 2 ** attempt);
5506
+ continue;
5507
+ }
5508
+ const body = (await response.text()).slice(0, 1024);
5509
+ throw new CodeHostHttpError(redact(`Code host request failed (${response.status} ${response.statusText}): ${body}`, secrets), response.status);
5510
+ }
5511
+ } };
5512
+ }
5513
+ function createCodeHostSuccessThrottle(sleep = (milliseconds) => Bun.sleep(milliseconds)) {
5514
+ let notBefore = 0;
5515
+ let waitInFlight;
5516
+ const waitUntilReady = async () => {
5517
+ while (true) {
5518
+ const deadline = notBefore;
5519
+ const remaining = deadline - Date.now();
5520
+ if (remaining <= 0) {
5521
+ if (notBefore === deadline) notBefore = 0;
5522
+ return;
5523
+ }
5524
+ await sleep(remaining);
5525
+ if (notBefore <= deadline) {
5526
+ notBefore = 0;
5527
+ return;
5528
+ }
5529
+ }
5530
+ };
5795
5531
  return {
5796
- kind: "review",
5797
- provider,
5798
- diffManifest,
5799
- review: redactedPublication.validated.review,
5800
- validated: redactedPublication.validated,
5801
- publicationPlan,
5802
- mainComment: publicationPlan.mainComment,
5803
- inlineCommentDrafts: publishing.inlineCommentDrafts,
5804
- taskChecks: redactedPublication.taskChecks,
5805
- repairAttempted: output.repairAttempted
5532
+ wait() {
5533
+ if (waitInFlight) return waitInFlight;
5534
+ const currentWait = waitUntilReady().finally(() => {
5535
+ if (waitInFlight === currentWait) waitInFlight = void 0;
5536
+ });
5537
+ waitInFlight = currentWait;
5538
+ return waitInFlight;
5539
+ },
5540
+ update(retryAfter) {
5541
+ const delay = retryAfterMilliseconds(retryAfter);
5542
+ if (delay > 0) notBefore = Math.max(notBefore, Date.now() + delay);
5543
+ }
5806
5544
  };
5807
5545
  }
5808
- function publishFailedRunTaskChecks(options, taskChecks) {
5809
- const redacted = redactCommandPublication({
5810
- body: "",
5811
- taskChecks,
5812
- redactor: options.secretRedactor
5813
- });
5814
- publishTaskChecks(options.checkSink, redacted.taskChecks);
5546
+ function shouldRetryCodeHostRequest(options) {
5547
+ return (options.method === "GET" || options.method === "HEAD" || options.retryStatuses?.includes(options.response.status) === true) && options.attempt < options.maxRetries && (options.response.status === 429 || options.response.status >= 500 || options.retryStatuses?.includes(options.response.status) === true);
5815
5548
  }
5816
- function commandResponseResultFromOutput(options) {
5817
- const commandResponse = options.output.commandResponse;
5818
- if (!commandResponse) return;
5819
- if (!options.commandInvocation) throw new Error("ctx.command.reply(...) is only available for command-triggered tasks");
5820
- return commandResponseRuntimeResult({
5821
- ...options,
5822
- commandResponse,
5823
- commandInvocation: options.commandInvocation
5824
- });
5549
+ function retryAfterMilliseconds(value) {
5550
+ if (!value) return 0;
5551
+ const seconds = Number(value);
5552
+ if (Number.isFinite(seconds) && seconds >= 0) return seconds * 1e3;
5553
+ const date = Date.parse(value);
5554
+ return Number.isFinite(date) ? Math.max(0, date - Date.now()) : 0;
5825
5555
  }
5826
- function assertReviewCommentOutput(output, hasCommandInvocation) {
5827
- if (output.comment) return;
5828
- throw new Error(hasCommandInvocation ? "ctx.comment(...) or ctx.command.reply(...) must be called exactly once per selected run" : "ctx.comment(...) must be called exactly once per selected run");
5556
+ function redact(value, secrets) {
5557
+ let redacted = value;
5558
+ for (const secret of secrets) redacted = redacted.split(secret).join("***");
5559
+ return redacted;
5829
5560
  }
5830
- async function runSynchronizeVerifier(options) {
5831
- if (options.options.event.rawAction !== "synchronize") return {
5832
- priorReviewState: options.priorReviewState,
5833
- threadActions: [],
5834
- providerModels: []
5561
+ //#endregion
5562
+ //#region src/hosts/azure-devops/coordinates.ts
5563
+ function azureOrganizationFromUrl(value) {
5564
+ const url = new URL(value);
5565
+ return url.hostname === "dev.azure.com" ? url.pathname.split("/").filter(Boolean)[0] : url.hostname.split(".")[0];
5566
+ }
5567
+ //#endregion
5568
+ //#region src/hosts/azure-devops/client.ts
5569
+ const identitySchema = z.looseObject({
5570
+ displayName: z.string().optional(),
5571
+ providerDisplayName: z.string().optional(),
5572
+ uniqueName: z.string().optional(),
5573
+ id: z.string().optional(),
5574
+ properties: z.looseObject({ Account: z.looseObject({ $value: z.string().optional() }).optional() }).optional()
5575
+ }).transform((identity) => {
5576
+ const displayName = identity.displayName ?? identity.providerDisplayName;
5577
+ const uniqueName = identity.uniqueName ?? identity.properties?.Account?.$value;
5578
+ return {
5579
+ ...identity,
5580
+ ...displayName ? { displayName } : {},
5581
+ ...uniqueName ? { uniqueName } : {}
5835
5582
  };
5836
- const config = options.config;
5837
- return await runInternalVerifier({
5838
- workspace: options.options.workspace,
5839
- config,
5840
- event: options.options.event,
5841
- provider: options.provider,
5842
- verifierProvider: resolveProvider(config, config.publication.autoResolve.model ?? config.defaultProvider),
5843
- plan: options.options.plan,
5844
- env: options.options.env,
5845
- piExecutable: options.options.piExecutable,
5846
- piRunner: options.options.piRunner,
5847
- log: options.options.log,
5848
- diffManifest: options.diffManifest,
5849
- priorReviewState: options.priorReviewState,
5850
- threadContexts: await options.options.loadInlineThreadContexts?.() ?? [],
5851
- mode: { kind: "synchronize" },
5852
- runId: options.runId,
5853
- piRunSink: options.piRunSink
5583
+ });
5584
+ const commitSchema = z.looseObject({ commitId: z.string().min(1) });
5585
+ const forkRepositorySchema = z.looseObject({ remoteUrl: z.string().min(1) });
5586
+ const pullRequestSchema$1 = z.looseObject({
5587
+ pullRequestId: z.number().int().positive(),
5588
+ isDraft: z.boolean().optional(),
5589
+ title: z.string(),
5590
+ description: z.string().nullish(),
5591
+ url: z.string().optional(),
5592
+ sourceRefName: z.string().min(1),
5593
+ targetRefName: z.string().min(1),
5594
+ createdBy: identitySchema.optional(),
5595
+ lastMergeSourceCommit: commitSchema,
5596
+ lastMergeTargetCommit: commitSchema,
5597
+ forkSource: z.looseObject({ repository: forkRepositorySchema }).optional(),
5598
+ repository: z.looseObject({
5599
+ id: z.string().min(1),
5600
+ name: z.string().min(1),
5601
+ url: z.string().optional(),
5602
+ project: z.looseObject({
5603
+ id: z.string().min(1),
5604
+ name: z.string().min(1)
5605
+ })
5606
+ })
5607
+ });
5608
+ const repositorySchema$1 = z.looseObject({
5609
+ id: z.string().min(1),
5610
+ name: z.string().min(1),
5611
+ project: z.looseObject({
5612
+ id: z.string().min(1),
5613
+ name: z.string().min(1)
5614
+ })
5615
+ });
5616
+ const iterationSchema = z.looseObject({
5617
+ id: z.number().int().positive(),
5618
+ sourceRefCommit: commitSchema
5619
+ });
5620
+ const iterationChangeSchema = z.looseObject({
5621
+ changeTrackingId: z.number().int(),
5622
+ changeType: z.string(),
5623
+ item: z.looseObject({
5624
+ path: z.string().min(1),
5625
+ originalPath: z.string().min(1).optional()
5626
+ })
5627
+ });
5628
+ const threadCommentSchema = z.looseObject({
5629
+ id: z.union([z.number(), z.string()]).transform(String),
5630
+ parentCommentId: z.number().int().optional(),
5631
+ content: z.string().default(""),
5632
+ author: identitySchema.optional(),
5633
+ isDeleted: z.boolean().optional()
5634
+ });
5635
+ const pointSchema = z.looseObject({
5636
+ line: z.number().int(),
5637
+ offset: z.number().int()
5638
+ });
5639
+ const threadSchema = z.looseObject({
5640
+ id: z.union([z.number(), z.string()]).transform(String),
5641
+ status: z.string().optional(),
5642
+ comments: z.array(threadCommentSchema).default([]),
5643
+ threadContext: z.looseObject({
5644
+ filePath: z.string().optional(),
5645
+ leftFileStart: pointSchema.nullish(),
5646
+ leftFileEnd: pointSchema.nullish(),
5647
+ rightFileStart: pointSchema.nullish(),
5648
+ rightFileEnd: pointSchema.nullish()
5649
+ }).nullish(),
5650
+ pullRequestThreadContext: z.looseObject({
5651
+ changeTrackingId: z.number().int().optional(),
5652
+ iterationContext: z.looseObject({
5653
+ firstComparingIteration: z.number().int(),
5654
+ secondComparingIteration: z.number().int()
5655
+ }).optional()
5656
+ }).nullish()
5657
+ });
5658
+ const statusSchema$1 = z.looseObject({ id: z.union([z.number(), z.string()]).transform(String) });
5659
+ const aclSchema = collectionSchema(z.looseObject({
5660
+ inheritPermissions: z.boolean().default(true),
5661
+ acesDictionary: z.record(z.string(), z.looseObject({
5662
+ allow: z.number().int().default(0),
5663
+ deny: z.number().int().default(0),
5664
+ extendedInfo: z.looseObject({
5665
+ effectiveAllow: z.number().int().default(0),
5666
+ effectiveDeny: z.number().int().default(0)
5667
+ }).optional()
5668
+ }))
5669
+ }));
5670
+ function createAzureDevOpsClient(env = process.env, fetch = globalThis.fetch, options = {}) {
5671
+ const organization = env.AZURE_DEVOPS_ORGANIZATION ?? organizationFromCollectionUri$1(env.SYSTEM_COLLECTIONURI);
5672
+ const project = env.AZURE_DEVOPS_PROJECT ?? env.SYSTEM_TEAMPROJECT;
5673
+ const pat = env.AZURE_DEVOPS_TOKEN;
5674
+ const bearerToken = env.AZURE_DEVOPS_BEARER_TOKEN ?? env.SYSTEM_ACCESSTOKEN;
5675
+ if (!organization) throw new Error("AZURE_DEVOPS_ORGANIZATION is required for Azure DevOps API calls");
5676
+ if (!project) throw new Error("AZURE_DEVOPS_PROJECT or SYSTEM_TEAMPROJECT is required for Azure DevOps API calls");
5677
+ if (!pat && !bearerToken) throw new Error("AZURE_DEVOPS_TOKEN, AZURE_DEVOPS_BEARER_TOKEN, or SYSTEM_ACCESSTOKEN is required for Azure DevOps API calls");
5678
+ const headers = pat ? { Authorization: `Basic ${Buffer.from(`:${pat}`).toString("base64")}` } : { Authorization: `Bearer ${bearerToken}` };
5679
+ const successThrottle = createCodeHostSuccessThrottle(options.sleep);
5680
+ const api = createCodeHostHttpClient({
5681
+ baseUrl: `https://dev.azure.com/${encodeURIComponent(organization)}/${encodeURIComponent(project)}/_apis/`,
5682
+ headers,
5683
+ fetch,
5684
+ successThrottle
5854
5685
  });
5855
- }
5856
- function createTaskContext(options) {
5857
- const repositorySlugParts = options.event.repository.slug.split("/");
5858
- let taskContext;
5859
- taskContext = {
5860
- run: { id: options.runId },
5861
- repository: {
5862
- root: options.workspace,
5863
- owner: repositorySlugParts.length > 1 ? repositorySlugParts[0] : void 0,
5864
- name: repositorySlugParts.at(-1) ?? "repo"
5686
+ const organizationApi = createCodeHostHttpClient({
5687
+ baseUrl: `https://dev.azure.com/${encodeURIComponent(organization)}/_apis/`,
5688
+ headers,
5689
+ fetch,
5690
+ successThrottle
5691
+ });
5692
+ const identityApi = createCodeHostHttpClient({
5693
+ baseUrl: `https://vssps.dev.azure.com/${encodeURIComponent(organization)}/_apis/`,
5694
+ headers,
5695
+ fetch,
5696
+ successThrottle
5697
+ });
5698
+ const pullRequestPath = (repositoryId, changeNumber) => `git/repositories/${encodeURIComponent(repositoryId)}/pullRequests/${changeNumber}`;
5699
+ return {
5700
+ organization,
5701
+ project,
5702
+ async currentUser() {
5703
+ return (await organizationApi.json("connectionData?connectOptions=1&lastChangeId=-1&lastChangeId64=-1", z.looseObject({ authenticatedUser: identitySchema }))).authenticatedUser;
5704
+ },
5705
+ async getRepository(repository) {
5706
+ const value = await api.json(withApiVersion(`git/repositories/${encodeURIComponent(repository)}`), repositorySchema$1);
5707
+ return {
5708
+ id: value.id,
5709
+ name: value.name,
5710
+ projectId: value.project.id,
5711
+ project: value.project.name
5712
+ };
5713
+ },
5714
+ async getRepositoryPermission(actor, projectId, repositoryId) {
5715
+ const candidates = (await identityApi.json(`identities?searchFilter=General&filterValue=${encodeURIComponent(actor)}&queryMembership=ExpandedUp&api-version=7.1`, collectionSchema(z.looseObject({
5716
+ descriptor: z.string().min(1),
5717
+ isActive: z.boolean().optional(),
5718
+ isContainer: z.boolean().optional(),
5719
+ memberOf: z.array(z.union([z.string().min(1), z.looseObject({ descriptor: z.string().min(1) })])).default([])
5720
+ })))).value.filter((identity) => identity.isActive !== false && identity.isContainer !== true);
5721
+ if (candidates.length !== 1) return "none";
5722
+ const identity = candidates[0];
5723
+ if (!identity) return "none";
5724
+ const descriptors = [identity.descriptor, ...identity.memberOf.map((group) => typeof group === "string" ? group : group.descriptor)];
5725
+ const loadAcls = (token) => {
5726
+ const query = new URLSearchParams({
5727
+ token,
5728
+ descriptors: descriptors.join(","),
5729
+ includeExtendedInfo: "true",
5730
+ recurse: "false",
5731
+ "api-version": "7.1"
5732
+ });
5733
+ return organizationApi.json(`accesscontrollists/2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87?${query}`, aclSchema);
5734
+ };
5735
+ const [projectAcls, repositoryAcls] = await Promise.all([loadAcls(`repoV2/${projectId}`), loadAcls(`repoV2/${projectId}/${repositoryId}`)]);
5736
+ return azurePermissionFromAcls(repositoryAcls.value.every((acl) => acl.inheritPermissions) ? [...projectAcls.value, ...repositoryAcls.value] : repositoryAcls.value);
5865
5737
  },
5866
- change: {
5867
- number: options.event.change.number,
5868
- title: options.event.change.title,
5869
- description: options.event.change.description,
5870
- url: options.event.change.url,
5871
- author: options.event.change.author,
5872
- base: options.event.change.base,
5873
- head: options.event.change.head,
5874
- isFork: options.event.change.isFork,
5875
- async diffManifest(manifestOptions) {
5876
- const key = JSON.stringify(manifestOptions ?? {});
5877
- const cached = options.manifestCache.get(key);
5878
- if (cached) return cloneDiffManifest(cached);
5879
- const manifest = projectDiffManifest(options.diffManifest, manifestOptions);
5880
- options.manifestCache.set(key, manifest);
5881
- return cloneDiffManifest(manifest);
5882
- },
5883
- async changedFiles() {
5884
- return options.diffManifest.files.map((file) => ({
5885
- path: file.path,
5886
- previousPath: file.previousPath,
5887
- status: file.status
5888
- }));
5889
- },
5890
- async currentHeadSha() {
5891
- return options.event.change.head.sha;
5892
- }
5738
+ getPullRequest: (repositoryId, changeNumber) => api.json(withApiVersion(pullRequestPath(repositoryId, changeNumber)), pullRequestSchema$1),
5739
+ async listIterations(repositoryId, changeNumber) {
5740
+ return (await api.json(withApiVersion(`${pullRequestPath(repositoryId, changeNumber)}/iterations`), collectionSchema(iterationSchema))).value.map((iteration) => ({
5741
+ id: iteration.id,
5742
+ headSha: iteration.sourceRefCommit.commitId
5743
+ }));
5893
5744
  },
5894
- platform: { id: options.event.platform.id },
5895
- command: options.commandInvocation ? {
5896
- name: options.commandInvocation.name,
5897
- line: options.commandInvocation.line,
5898
- arguments: { ...options.commandInvocation.arguments },
5899
- async reply(markdown) {
5900
- collectCommandResponse(options.output, markdown, options.taskName);
5745
+ async loadChange(options) {
5746
+ if (options.organization !== organization || options.project !== project) throw new Error("Azure DevOps client coordinates do not match its configured organization and project");
5747
+ const pullRequest = await this.getPullRequest(options.repositoryId, options.changeNumber);
5748
+ const iterations = await this.listIterations(options.repositoryId, options.changeNumber);
5749
+ const headSha = pullRequest.lastMergeSourceCommit.commitId;
5750
+ const iteration = iterations.findLast((candidate) => candidate.headSha === headSha);
5751
+ if (!iteration) throw new Error(`Azure DevOps has no pull request iteration for head ${headSha}`);
5752
+ const sourceRef = branchName(pullRequest.sourceRefName);
5753
+ const targetRef = branchName(pullRequest.targetRefName);
5754
+ return {
5755
+ repository: {
5756
+ slug: `${organization}/${pullRequest.repository.project.name}/${pullRequest.repository.name}`,
5757
+ url: repositoryWebUrl(organization, pullRequest.repository.project.name, pullRequest.repository.name)
5758
+ },
5759
+ coordinates: {
5760
+ provider: "azure-devops",
5761
+ organization,
5762
+ project: pullRequest.repository.project.name,
5763
+ projectId: pullRequest.repository.project.id,
5764
+ repositoryId: pullRequest.repository.id
5765
+ },
5766
+ change: {
5767
+ number: pullRequest.pullRequestId,
5768
+ isDraft: pullRequest.isDraft,
5769
+ title: pullRequest.title,
5770
+ description: pullRequest.description ?? "",
5771
+ url: `${repositoryWebUrl(organization, pullRequest.repository.project.name, pullRequest.repository.name)}/pullrequest/${pullRequest.pullRequestId}`,
5772
+ author: pullRequest.createdBy?.uniqueName ? { login: pullRequest.createdBy.uniqueName } : void 0,
5773
+ base: {
5774
+ sha: pullRequest.lastMergeTargetCommit.commitId,
5775
+ ref: targetRef
5776
+ },
5777
+ head: {
5778
+ sha: headSha,
5779
+ ref: sourceRef,
5780
+ ...pullRequest.forkSource?.repository.remoteUrl ? { url: pullRequest.forkSource.repository.remoteUrl } : {}
5781
+ },
5782
+ ...pullRequest.forkSource ? { isFork: true } : {}
5783
+ },
5784
+ iterationId: iteration.id
5785
+ };
5786
+ },
5787
+ async listIterationChanges(repositoryId, changeNumber, iterationId) {
5788
+ const changes = [];
5789
+ let skip = 0;
5790
+ let top = 2e3;
5791
+ for (;;) {
5792
+ const path = `${pullRequestPath(repositoryId, changeNumber)}/iterations/${iterationId}/changes?compareTo=0&$skip=${skip}&$top=${top}`;
5793
+ const page = await api.json(withApiVersion(path), z.looseObject({
5794
+ changeEntries: z.array(iterationChangeSchema),
5795
+ nextSkip: z.number().int().nonnegative().optional(),
5796
+ nextTop: z.number().int().nonnegative().max(2e3).optional()
5797
+ }));
5798
+ changes.push(...page.changeEntries.map((change) => ({
5799
+ changeTrackingId: change.changeTrackingId,
5800
+ changeType: change.changeType,
5801
+ path: trimLeadingSlash(change.item.path),
5802
+ ...change.item.originalPath ? { originalPath: trimLeadingSlash(change.item.originalPath) } : {}
5803
+ })));
5804
+ if (!page.nextSkip) return changes;
5805
+ skip = page.nextSkip;
5806
+ if (page.nextTop) top = page.nextTop;
5901
5807
  }
5902
- } : void 0,
5903
- secret(secret) {
5904
- return resolveTaskSecret(secret, options);
5905
5808
  },
5906
- pi: { async run(agent, input, runOptions) {
5907
- const result = await runReviewAgent({
5908
- agent,
5909
- input,
5910
- runOptions,
5911
- runtime: {
5912
- ...options,
5913
- taskContext,
5914
- runId: options.runId,
5915
- piRunSink: options.piRunSink
5916
- }
5917
- });
5918
- options.output.providerModels.push(...result.providerModels);
5919
- if (result.repairAttempted) options.output.repairAttempted = true;
5920
- trackResultFindingScope(options.output, result.value, runOptions?.paths);
5921
- return agentOutputForTaskContext(agent, result.value);
5922
- } },
5923
- review: { async prior() {
5924
- return priorReviewForTask(options.priorMainComment, options.priorReviewState);
5925
- } },
5926
- check: createCheckHandle(options.output),
5927
- async comment(value) {
5928
- collectComment(options.output, value, options.taskName);
5809
+ async listThreads(repositoryId, changeNumber) {
5810
+ return (await api.json(withApiVersion(`${pullRequestPath(repositoryId, changeNumber)}/threads`), collectionSchema(threadSchema))).value;
5929
5811
  },
5930
- log: options.taskLog ?? console
5812
+ createThread: (repositoryId, changeNumber, body) => api.json(withApiVersion(`${pullRequestPath(repositoryId, changeNumber)}/threads`), threadSchema, jsonRequest$2("POST", body)),
5813
+ updateComment: (repositoryId, changeNumber, threadId, commentId, content) => api.json(withApiVersion(`${pullRequestPath(repositoryId, changeNumber)}/threads/${encodeURIComponent(threadId)}/comments/${encodeURIComponent(commentId)}`), threadCommentSchema, jsonRequest$2("PATCH", { content })),
5814
+ createThreadComment: (repositoryId, changeNumber, threadId, body) => api.json(withApiVersion(`${pullRequestPath(repositoryId, changeNumber)}/threads/${encodeURIComponent(threadId)}/comments`), threadCommentSchema, jsonRequest$2("POST", body)),
5815
+ updateThreadStatus: (repositoryId, changeNumber, threadId, status) => api.json(withApiVersion(`${pullRequestPath(repositoryId, changeNumber)}/threads/${encodeURIComponent(threadId)}`), threadSchema, jsonRequest$2("PATCH", { status })),
5816
+ async createStatus(repositoryId, changeNumber, body) {
5817
+ return (await api.json(withApiVersion(`${pullRequestPath(repositoryId, changeNumber)}/statuses`), statusSchema$1, jsonRequest$2("POST", body))).id;
5818
+ }
5931
5819
  };
5932
- return taskContext;
5933
5820
  }
5934
- function agentOutputForTaskContext(_agent, value) {
5935
- return value;
5821
+ function azureDevOpsStatusState(state) {
5822
+ switch (state) {
5823
+ case "pending": return "pending";
5824
+ case "success": return "succeeded";
5825
+ case "failure": return "failed";
5826
+ case "neutral": return "notApplicable";
5827
+ }
5936
5828
  }
5937
- function resolveTaskSecret(secret, options) {
5938
- if (secret.kind !== "pipr.secret" || typeof secret.name !== "string") throw new Error("ctx.secret(...) requires a pipr.secret reference");
5939
- const value = (options.env ?? process.env)[secret.name];
5940
- if (!value) throw new Error(`Missing secret env var: ${secret.name}`);
5941
- options.log?.addSecret(value);
5942
- options.secretRedactor?.addSecret(value);
5943
- return value;
5829
+ function azureRepositoryPermission(bits) {
5830
+ if ((bits & 2) === 0) return "none";
5831
+ if ((bits & 8192) !== 0) return "admin";
5832
+ if ((bits & 2048) !== 0) return "maintain";
5833
+ if ((bits & 4) !== 0) return "write";
5834
+ if ((bits & 16384) !== 0) return "triage";
5835
+ return "read";
5944
5836
  }
5945
- function commandResponseRuntimeResult(options) {
5946
- const redacted = redactCommandPublication({
5947
- body: options.commandResponse.value,
5948
- taskChecks: options.taskChecks,
5949
- redactor: options.secretRedactor
5950
- });
5951
- return {
5952
- kind: "command-response",
5953
- provider: options.provider,
5954
- diffManifest: options.diffManifest,
5955
- taskChecks: redacted.taskChecks,
5956
- repairAttempted: options.output.repairAttempted,
5957
- commandResponse: {
5958
- commandName: options.commandInvocation.name,
5959
- line: options.commandInvocation.line,
5960
- arguments: options.commandInvocation.arguments,
5961
- body: redacted.body
5962
- }
5963
- };
5837
+ function azurePermissionFromAcls(acls) {
5838
+ let allow = 0;
5839
+ let deny = 0;
5840
+ for (const acl of acls) for (const ace of Object.values(acl.acesDictionary)) {
5841
+ allow |= ace.extendedInfo?.effectiveAllow ?? ace.allow;
5842
+ deny |= ace.extendedInfo?.effectiveDeny ?? ace.deny;
5843
+ }
5844
+ return azureRepositoryPermission(allow & ~deny);
5964
5845
  }
5965
- function publishTaskChecks(sink, checks) {
5966
- for (const check of checks) sink?.setTaskResult(check);
5846
+ function collectionSchema(item) {
5847
+ return z.looseObject({
5848
+ count: z.number().int().nonnegative(),
5849
+ value: z.array(item)
5850
+ });
5967
5851
  }
5968
- function skippedTaskRuntimeResult(options) {
5969
- const reason = options.reason ?? (options.taskName ? `Task '${options.taskName}' was not registered` : "No tasks matched the change request event");
5970
- const review = {
5971
- summary: { body: reason },
5972
- inlineFindings: []
5973
- };
5974
- const validated = {
5975
- review,
5976
- validFindings: [],
5977
- droppedFindings: []
5978
- };
5979
- const publicationPlan = buildCommentPublishingPlan({
5980
- event: options.event,
5981
- main: reason,
5982
- validated,
5983
- manifest: options.diffManifest,
5984
- maxInlineComments: options.config.publication.maxInlineComments,
5985
- maxStoredFindings: options.config.publication.maxStoredFindings,
5986
- showHeader: options.config.publication.showHeader,
5987
- showFooter: options.config.publication.showFooter,
5988
- showStats: options.config.publication.showStats,
5989
- metadata: {
5990
- runtimeVersion,
5991
- configVersion: options.versionCompatibility?.configVersion,
5992
- trustedConfigSha: options.trustedConfigSha,
5993
- trustedConfigHash: options.trustedConfigHash,
5994
- reviewedHeadSha: options.event.change.head.sha,
5995
- providerModels: [options.provider.model],
5996
- selectedTasks: [],
5997
- failedTasks: [],
5998
- validFindings: 0,
5999
- droppedFindings: 0
6000
- }
6001
- }).publicationPlan;
6002
- return {
6003
- kind: "skipped",
6004
- skipReason: reason,
6005
- provider: options.provider,
6006
- diffManifest: options.diffManifest,
6007
- review,
6008
- validated,
6009
- publicationPlan,
6010
- mainComment: publicationPlan.mainComment,
6011
- inlineCommentDrafts: [],
6012
- taskChecks: [],
6013
- repairAttempted: false
6014
- };
5852
+ function withApiVersion(path) {
5853
+ return `${path}${path.includes("?") ? "&" : "?"}api-version=7.1`;
6015
5854
  }
6016
- //#endregion
6017
- //#region src/shared/secret-redactor-core.ts
6018
- const redactedSecret = "[redacted secret]";
6019
- const minimumSecretLength = 4;
6020
- function createSecretRedactor(initialSecrets) {
6021
- const secrets = new Set(initialSecrets.filter((value) => value.length >= minimumSecretLength));
5855
+ function jsonRequest$2(method, body) {
6022
5856
  return {
6023
- addSecret(value) {
6024
- if (value && value.length >= minimumSecretLength) secrets.add(value);
6025
- },
6026
- redact(value) {
6027
- const ordered = [...secrets].sort((left, right) => right.length - left.length);
6028
- let next = value;
6029
- for (const secret of ordered) next = next.replaceAll(secret, redactedSecret);
6030
- return {
6031
- value: next,
6032
- detected: next !== value
6033
- };
6034
- }
5857
+ method,
5858
+ headers: { "Content-Type": "application/json" },
5859
+ body: JSON.stringify(body)
6035
5860
  };
6036
5861
  }
6037
- //#endregion
6038
- //#region src/shared/secret-redactor.ts
6039
- function createKnownSecretRedactor(options) {
6040
- return createSecretRedactor(sensitiveEnvironmentValues(options?.env ?? process.env));
5862
+ function organizationFromCollectionUri$1(value) {
5863
+ return value ? azureOrganizationFromUrl(value) : void 0;
5864
+ }
5865
+ function branchName(ref) {
5866
+ return ref.replace(/^refs\/heads\//, "");
5867
+ }
5868
+ function repositoryWebUrl(organization, project, repository) {
5869
+ return `https://dev.azure.com/${encodeURIComponent(organization)}/${encodeURIComponent(project)}/_git/${encodeURIComponent(repository)}`;
5870
+ }
5871
+ function trimLeadingSlash(value) {
5872
+ return value.replace(/^\/+/, "");
6041
5873
  }
6042
5874
  //#endregion
6043
5875
  //#region src/hosts/env.ts
@@ -6417,8 +6249,17 @@ async function loadAzureDevOpsPriorReviewState(options) {
6417
6249
  const state = extractPriorReviewState(await loadAzureDevOpsPriorMainComment(options), options.change.change.number);
6418
6250
  if (!state) return void 0;
6419
6251
  const owner = await authenticatedAzureOwner(options.client);
6420
- const bodies = ownedThreadComments(await options.client.listThreads(azureCoordinates$1(options.change).repositoryId, options.change.change.number), owner.uniqueName).map((comment) => comment.content);
6421
- return applyResolvedFindingMarkers(applyInlineFindingMarkers(state, bodies), bodies);
6252
+ const threads = await options.client.listThreads(azureCoordinates$1(options.change).repositoryId, options.change.change.number);
6253
+ const bodies = ownedThreadComments(threads, owner.uniqueName).map((comment) => comment.content);
6254
+ return applyNativeThreadResolutions(applyResolvedFindingMarkers(applyInlineFindingMarkers(state, bodies), bodies), threads.flatMap((thread) => {
6255
+ const root = thread.comments[0];
6256
+ const marker = root ? extractInlineFindingMarkerRecords([root.content])[0] : void 0;
6257
+ return root && marker && root.author?.uniqueName === owner.uniqueName ? [{
6258
+ findingId: marker.id,
6259
+ findingHeadSha: marker.head,
6260
+ resolved: isResolved(thread)
6261
+ }] : [];
6262
+ }));
6422
6263
  }
6423
6264
  async function loadAzureDevOpsPriorMainComment(options) {
6424
6265
  const owner = await authenticatedAzureOwner(options.client);
@@ -6765,19 +6606,230 @@ function createAzureDevOpsHostAdapter(options = {}) {
6765
6606
  };
6766
6607
  }
6767
6608
  }
6768
- };
6769
- }
6770
- function azureCoordinates(change) {
6771
- if (change.coordinates?.provider !== "azure-devops") throw new Error("Azure DevOps adapter requires Azure DevOps coordinates");
6772
- return change.coordinates;
6609
+ };
6610
+ }
6611
+ function azureCoordinates(change) {
6612
+ if (change.coordinates?.provider !== "azure-devops") throw new Error("Azure DevOps adapter requires Azure DevOps coordinates");
6613
+ return change.coordinates;
6614
+ }
6615
+ function azureCoordinatesFromRepository(client, slug) {
6616
+ const repositoryId = slug.split("/").at(-1);
6617
+ if (!repositoryId) throw new Error("Azure DevOps repository slug must identify a repository");
6618
+ return {
6619
+ organization: client.organization,
6620
+ project: client.project,
6621
+ repositoryId
6622
+ };
6623
+ }
6624
+ //#endregion
6625
+ //#region src/hosts/bitbucket/schema.ts
6626
+ const bitbucketRepositorySchema = z.looseObject({
6627
+ uuid: z.string().min(1),
6628
+ name: z.string().min(1),
6629
+ full_name: z.string().min(1),
6630
+ slug: z.string().min(1).optional(),
6631
+ links: z.looseObject({ html: z.looseObject({ href: z.string().url() }) })
6632
+ }).transform((repository) => ({
6633
+ ...repository,
6634
+ slug: repository.slug ?? repository.full_name.split("/").at(-1) ?? repository.full_name
6635
+ }));
6636
+ //#endregion
6637
+ //#region src/hosts/bitbucket/client.ts
6638
+ const userSchema$1 = z.looseObject({
6639
+ uuid: z.string().optional(),
6640
+ nickname: z.string().optional(),
6641
+ display_name: z.string().optional()
6642
+ });
6643
+ const endpointSchema = z.looseObject({
6644
+ branch: z.looseObject({ name: z.string().min(1) }),
6645
+ commit: z.looseObject({ hash: z.string().min(1) }),
6646
+ repository: bitbucketRepositorySchema
6647
+ });
6648
+ const pullRequestSchema = z.looseObject({
6649
+ id: z.number().int().positive(),
6650
+ draft: z.boolean().optional(),
6651
+ title: z.string(),
6652
+ description: z.string().default(""),
6653
+ author: userSchema$1.optional(),
6654
+ source: endpointSchema,
6655
+ destination: endpointSchema,
6656
+ links: z.looseObject({ html: z.looseObject({ href: z.string().url() }) })
6657
+ });
6658
+ const inlineSchema = z.looseObject({
6659
+ path: z.string().optional(),
6660
+ from: z.number().int().nullable().optional(),
6661
+ to: z.number().int().nullable().optional(),
6662
+ start_from: z.number().int().nullable().optional(),
6663
+ start_to: z.number().int().nullable().optional()
6664
+ });
6665
+ const commentSchema = z.looseObject({
6666
+ id: z.union([z.number(), z.string()]).transform(String),
6667
+ content: z.looseObject({ raw: z.string().default("") }),
6668
+ user: userSchema$1.optional(),
6669
+ parent: z.looseObject({ id: z.union([z.number(), z.string()]).transform(String) }).optional(),
6670
+ inline: inlineSchema.optional(),
6671
+ deleted: z.boolean().optional(),
6672
+ resolution: z.looseObject({}).optional()
6673
+ });
6674
+ function createBitbucketClient(env = process.env, fetch = globalThis.fetch) {
6675
+ const workspace = env.BITBUCKET_WORKSPACE;
6676
+ const repository = env.BITBUCKET_REPO_SLUG;
6677
+ const token = env.BITBUCKET_API_TOKEN;
6678
+ const email = env.BITBUCKET_EMAIL;
6679
+ if (!workspace) throw new Error("BITBUCKET_WORKSPACE is required for Bitbucket Cloud API calls");
6680
+ if (!repository) throw new Error("BITBUCKET_REPO_SLUG is required for Bitbucket Cloud API calls");
6681
+ if (!token || !email) throw new Error("BITBUCKET_EMAIL and BITBUCKET_API_TOKEN are required");
6682
+ const authorization = `Basic ${Buffer.from(`${email}:${token}`).toString("base64")}`;
6683
+ const repositoryApiPath = `/2.0/repositories/${encodeURIComponent(workspace)}/${encodeURIComponent(repository)}/`;
6684
+ const api = createCodeHostHttpClient({
6685
+ baseUrl: `https://api.bitbucket.org${repositoryApiPath}`,
6686
+ headers: { Authorization: authorization },
6687
+ fetch
6688
+ });
6689
+ const rootApi = createCodeHostHttpClient({
6690
+ baseUrl: "https://api.bitbucket.org/2.0/",
6691
+ headers: { Authorization: authorization },
6692
+ fetch
6693
+ });
6694
+ const prPath = (id) => `pullrequests/${id}`;
6695
+ return {
6696
+ workspace,
6697
+ repository,
6698
+ async currentUser() {
6699
+ const value = await rootApi.json("user", userSchema$1);
6700
+ return {
6701
+ uuid: value.uuid,
6702
+ nickname: value.nickname,
6703
+ displayName: value.display_name
6704
+ };
6705
+ },
6706
+ async getRepository() {
6707
+ const value = await api.json("", bitbucketRepositorySchema);
6708
+ return {
6709
+ uuid: value.uuid,
6710
+ slug: value.slug,
6711
+ fullName: value.full_name,
6712
+ url: value.links.html.href
6713
+ };
6714
+ },
6715
+ async getRepositoryPermission(actor, repositoryUuid) {
6716
+ const permissionEmail = env.BITBUCKET_PERMISSION_EMAIL;
6717
+ const permissionToken = env.BITBUCKET_PERMISSION_API_TOKEN;
6718
+ if (!permissionEmail || !permissionToken) throw new Error("BITBUCKET_PERMISSION_EMAIL and BITBUCKET_PERMISSION_API_TOKEN are required for Bitbucket permission checks");
6719
+ const permissionApi = createCodeHostHttpClient({
6720
+ baseUrl: "https://api.bitbucket.org/2.0/",
6721
+ headers: { Authorization: `Basic ${Buffer.from(`${permissionEmail}:${permissionToken}`).toString("base64")}` },
6722
+ fetch
6723
+ });
6724
+ const query = encodeURIComponent(`repository.uuid="${escapeBitbucketQueryValue(repositoryUuid)}" AND user.nickname="${escapeBitbucketQueryValue(actor)}"`);
6725
+ return (await permissionApi.json(`workspaces/${encodeURIComponent(workspace)}/permissions/repositories?q=${query}&pagelen=100`, pagedSchema(z.looseObject({
6726
+ permission: z.enum([
6727
+ "read",
6728
+ "write",
6729
+ "admin"
6730
+ ]),
6731
+ user: userSchema$1
6732
+ })))).values.find((entry) => entry.user.nickname === actor)?.permission ?? "none";
6733
+ },
6734
+ getPullRequest: (id) => api.json(prPath(id), pullRequestSchema),
6735
+ async loadChange(options) {
6736
+ if (options.workspace !== workspace || options.repository !== repository) throw new Error("Bitbucket client coordinates do not match the requested repository");
6737
+ const pullRequest = await this.getPullRequest(options.changeNumber);
6738
+ return {
6739
+ repository: {
6740
+ slug: pullRequest.destination.repository.full_name,
6741
+ url: pullRequest.destination.repository.links.html.href
6742
+ },
6743
+ coordinates: {
6744
+ provider: "bitbucket",
6745
+ workspace,
6746
+ repository,
6747
+ repositoryUuid: pullRequest.destination.repository.uuid
6748
+ },
6749
+ change: {
6750
+ number: pullRequest.id,
6751
+ isDraft: pullRequest.draft,
6752
+ title: pullRequest.title,
6753
+ description: pullRequest.description,
6754
+ url: pullRequest.links.html.href,
6755
+ author: pullRequest.author?.nickname ? { login: pullRequest.author.nickname } : void 0,
6756
+ base: {
6757
+ sha: pullRequest.destination.commit.hash,
6758
+ ref: pullRequest.destination.branch.name,
6759
+ url: pullRequest.destination.repository.links.html.href
6760
+ },
6761
+ head: {
6762
+ sha: pullRequest.source.commit.hash,
6763
+ ref: pullRequest.source.branch.name,
6764
+ url: pullRequest.source.repository.links.html.href
6765
+ },
6766
+ isFork: pullRequest.source.repository.uuid !== pullRequest.destination.repository.uuid
6767
+ }
6768
+ };
6769
+ },
6770
+ async listComments(id) {
6771
+ return (await listAll(api, `${prPath(id)}/comments`, commentSchema, repositoryApiPath)).filter((comment) => !comment.deleted);
6772
+ },
6773
+ createComment: (id, body) => api.json(`${prPath(id)}/comments`, commentSchema, jsonRequest$1("POST", body)),
6774
+ updateComment: (id, commentId, content) => api.json(`${prPath(id)}/comments/${encodeURIComponent(commentId)}`, commentSchema, jsonRequest$1("PUT", { content: { raw: content } })),
6775
+ async replyToComment(id, commentId, content) {
6776
+ const parentId = positiveCommentId(commentId);
6777
+ return await api.json(`${prPath(id)}/comments`, commentSchema, jsonRequest$1("POST", {
6778
+ content: { raw: content },
6779
+ parent: { id: parentId }
6780
+ }));
6781
+ },
6782
+ async resolveComment(id, commentId) {
6783
+ positiveCommentId(commentId);
6784
+ await api.json(`${prPath(id)}/comments/${encodeURIComponent(commentId)}/resolve`, z.unknown(), { method: "POST" });
6785
+ },
6786
+ async setStatus(sha, key, body) {
6787
+ return (await api.json(`commit/${encodeURIComponent(sha)}/statuses/build`, z.looseObject({ key: z.string().default(key) }), jsonRequest$1("POST", {
6788
+ ...body,
6789
+ key
6790
+ }))).key;
6791
+ }
6792
+ };
6793
+ }
6794
+ function positiveCommentId(value) {
6795
+ const id = Number(value);
6796
+ if (!Number.isSafeInteger(id) || id <= 0) throw new Error("Bitbucket comment ID must be a positive integer");
6797
+ return id;
6798
+ }
6799
+ function bitbucketStatusState(state) {
6800
+ if (state === "pending") return "INPROGRESS";
6801
+ if (state === "failure") return "FAILED";
6802
+ if (state === "neutral") return "STOPPED";
6803
+ return "SUCCESSFUL";
6804
+ }
6805
+ function escapeBitbucketQueryValue(value) {
6806
+ return value.replaceAll("\\", "\\\\").replaceAll("\"", "\\\"");
6807
+ }
6808
+ function pagedSchema(item) {
6809
+ return z.looseObject({
6810
+ values: z.array(item),
6811
+ next: z.string().url().optional()
6812
+ });
6813
+ }
6814
+ async function listAll(api, path, schema, allowedPathPrefix) {
6815
+ const values = [];
6816
+ let next = path;
6817
+ while (next) {
6818
+ const page = await api.json(next, pagedSchema(schema));
6819
+ values.push(...page.values);
6820
+ if (page.next) {
6821
+ const url = new URL(page.next);
6822
+ if (url.origin !== "https://api.bitbucket.org" || !url.pathname.startsWith(allowedPathPrefix)) throw new Error("Bitbucket pagination URL must stay inside the configured repository API");
6823
+ }
6824
+ next = page.next;
6825
+ }
6826
+ return values;
6773
6827
  }
6774
- function azureCoordinatesFromRepository(client, slug) {
6775
- const repositoryId = slug.split("/").at(-1);
6776
- if (!repositoryId) throw new Error("Azure DevOps repository slug must identify a repository");
6828
+ function jsonRequest$1(method, body) {
6777
6829
  return {
6778
- organization: client.organization,
6779
- project: client.project,
6780
- repositoryId
6830
+ method,
6831
+ headers: { "Content-Type": "application/json" },
6832
+ body: JSON.stringify(body)
6781
6833
  };
6782
6834
  }
6783
6835
  //#endregion
@@ -7027,7 +7079,14 @@ async function loadBitbucketPriorReviewState(options) {
7027
7079
  const state = extractPriorReviewState(body ? normalizeBitbucketMarkdown(body) : void 0, options.change.change.number);
7028
7080
  if (!state) return void 0;
7029
7081
  const bodies = comments.map((comment) => normalizeBitbucketMarkdown(comment.content.raw));
7030
- return applyResolvedFindingMarkers(applyInlineFindingMarkers(state, bodies), bodies);
7082
+ return applyNativeThreadResolutions(applyResolvedFindingMarkers(applyInlineFindingMarkers(state, bodies), bodies), comments.flatMap((comment) => {
7083
+ const marker = !comment.parent ? extractInlineFindingMarkerRecords([normalizeBitbucketMarkdown(comment.content.raw)])[0] : void 0;
7084
+ return marker ? [{
7085
+ findingId: marker.id,
7086
+ findingHeadSha: marker.head,
7087
+ resolved: comment.resolution !== void 0
7088
+ }] : [];
7089
+ }));
7031
7090
  }
7032
7091
  async function loadBitbucketPriorMainComment(options) {
7033
7092
  const body = (await loadBitbucketOwnedComments(options)).find((comment) => normalizeBitbucketMarkdown(comment.content.raw).includes(mainMarker$1(options.change.change.number)))?.content.raw;
@@ -7893,23 +7952,20 @@ async function loadGitHubPriorReviewState(options) {
7893
7952
  ownerLogin
7894
7953
  }), options.change.change.number);
7895
7954
  if (!state) return;
7896
- const inlineBodies = (await options.client.listReviewComments({
7897
- repo: options.change.repository.slug,
7898
- pullRequestNumber: options.change.change.number
7899
- })).filter((comment) => comment.authorLogin === ownerLogin).map((comment) => comment.body ?? "");
7900
- return applyResolvedFindingMarkers(applyInlineFindingMarkers(state, inlineBodies), inlineBodies);
7955
+ const { ownerComments, threadByCommentId } = await loadOwnedReviewThreads(options, ownerLogin);
7956
+ const inlineBodies = ownerComments.map((comment) => comment.body ?? "");
7957
+ return applyNativeThreadResolutions(applyResolvedFindingMarkers(applyInlineFindingMarkers(state, inlineBodies), inlineBodies), ownerComments.flatMap((comment) => {
7958
+ const marker = extractInlineFindingMarkerRecords([comment.body ?? ""])[0];
7959
+ const thread = threadByCommentId.get(comment.id);
7960
+ return marker && thread ? [{
7961
+ findingId: marker.id,
7962
+ findingHeadSha: marker.head,
7963
+ resolved: thread.isResolved
7964
+ }] : [];
7965
+ }));
7901
7966
  }
7902
7967
  async function loadGitHubInlineThreadContexts(options) {
7903
- const ownerLogin = await options.client.getAuthenticatedUserLogin();
7904
- const comments = await options.client.listReviewComments({
7905
- repo: options.change.repository.slug,
7906
- pullRequestNumber: options.change.change.number
7907
- });
7908
- const ownerComments = comments.filter((comment) => comment.authorLogin === ownerLogin);
7909
- const threadByCommentId = reviewThreadByCommentId(await options.client.listReviewThreads({
7910
- repo: options.change.repository.slug,
7911
- pullRequestNumber: options.change.change.number
7912
- }));
7968
+ const { comments, ownerComments, threadByCommentId } = await loadOwnedReviewThreads(options, await options.client.getAuthenticatedUserLogin());
7913
7969
  const commentById = new Map(comments.map((comment) => [comment.id, comment]));
7914
7970
  return ownerComments.flatMap((comment) => {
7915
7971
  const marker = extractInlineFindingMarkerRecords([comment.body ?? ""])[0];
@@ -7933,6 +7989,19 @@ async function loadGitHubInlineThreadContexts(options) {
7933
7989
  }];
7934
7990
  });
7935
7991
  }
7992
+ async function loadOwnedReviewThreads(options, ownerLogin) {
7993
+ const coordinates = {
7994
+ repo: options.change.repository.slug,
7995
+ pullRequestNumber: options.change.change.number
7996
+ };
7997
+ const comments = await options.client.listReviewComments(coordinates);
7998
+ const threads = await options.client.listReviewThreads(coordinates);
7999
+ return {
8000
+ comments,
8001
+ ownerComments: comments.filter((comment) => comment.authorLogin === ownerLogin),
8002
+ threadByCommentId: reviewThreadByCommentId(threads)
8003
+ };
8004
+ }
7936
8005
  async function loadGitHubPriorMainComment(options) {
7937
8006
  const ownerLogin = options.ownerLogin ?? await options.client.getAuthenticatedUserLogin();
7938
8007
  return findMainComment(await options.client.listIssueComments({
@@ -8477,6 +8546,213 @@ function createGitHubHostAdapter(options = {}) {
8477
8546
  };
8478
8547
  }
8479
8548
  //#endregion
8549
+ //#region src/hosts/gitlab/client.ts
8550
+ const userSchema = z.looseObject({
8551
+ id: z.number().int().positive(),
8552
+ username: z.string().min(1)
8553
+ });
8554
+ const noteSchema = z.looseObject({
8555
+ id: z.union([z.number(), z.string()]).transform(String),
8556
+ body: z.string(),
8557
+ author: userSchema.optional()
8558
+ });
8559
+ const positionSchema = z.looseObject({
8560
+ new_path: z.string().optional(),
8561
+ old_path: z.string().optional(),
8562
+ new_line: z.number().int().positive().nullish().transform((value) => value ?? void 0),
8563
+ old_line: z.number().int().positive().nullish().transform((value) => value ?? void 0),
8564
+ line_range: z.looseObject({
8565
+ start: z.looseObject({
8566
+ new_line: z.number().int().positive().nullish(),
8567
+ old_line: z.number().int().positive().nullish()
8568
+ }),
8569
+ end: z.looseObject({
8570
+ new_line: z.number().int().positive().nullish(),
8571
+ old_line: z.number().int().positive().nullish()
8572
+ })
8573
+ }).optional()
8574
+ });
8575
+ const discussionNoteSchema = noteSchema.extend({
8576
+ resolvable: z.boolean().optional(),
8577
+ resolved: z.boolean().optional(),
8578
+ position: positionSchema.nullable().optional()
8579
+ });
8580
+ const discussionSchema = z.looseObject({
8581
+ id: z.string().min(1),
8582
+ individual_note: z.boolean().optional(),
8583
+ notes: z.array(discussionNoteSchema)
8584
+ });
8585
+ const diffRefsSchema = z.looseObject({
8586
+ base_sha: z.string().min(1),
8587
+ start_sha: z.string().min(1),
8588
+ head_sha: z.string().min(1)
8589
+ });
8590
+ const mergeRequestResponseSchema = z.looseObject({
8591
+ iid: z.number().int().positive(),
8592
+ title: z.string().default(""),
8593
+ description: z.string().nullable().optional(),
8594
+ web_url: z.string().url().optional(),
8595
+ source_branch: z.string().min(1),
8596
+ target_branch: z.string().min(1),
8597
+ source_project_id: z.number().int().positive(),
8598
+ target_project_id: z.number().int().positive(),
8599
+ sha: z.string().min(1),
8600
+ draft: z.boolean().optional(),
8601
+ author: userSchema.optional(),
8602
+ references: z.looseObject({ full: z.string().optional() }).optional(),
8603
+ diff_refs: diffRefsSchema.nullable().optional()
8604
+ });
8605
+ const mergeRequestSchema = mergeRequestResponseSchema.extend({ diff_refs: diffRefsSchema });
8606
+ const memberSchema = z.looseObject({ access_level: z.number().int().min(0) });
8607
+ const statusSchema = z.looseObject({ id: z.union([z.number(), z.string()]).optional() });
8608
+ function createGitLabClient(env = process.env, fetch = globalThis.fetch, sleep = (milliseconds) => Bun.sleep(milliseconds)) {
8609
+ const token = env.GITLAB_TOKEN ?? env.CI_JOB_TOKEN;
8610
+ if (!token) throw new Error("GITLAB_TOKEN or CI_JOB_TOKEN is required for GitLab API calls");
8611
+ const headers = env.GITLAB_TOKEN ? { "PRIVATE-TOKEN": token } : { "JOB-TOKEN": token };
8612
+ const clientOptions = {
8613
+ baseUrl: `${(env.CI_API_V4_URL ?? "https://gitlab.com/api/v4").replace(/\/$/, "")}/`,
8614
+ headers,
8615
+ fetch,
8616
+ sleep: async (milliseconds) => void await sleep(milliseconds)
8617
+ };
8618
+ const api = createCodeHostHttpClient(clientOptions);
8619
+ const statusApi = createCodeHostHttpClient({
8620
+ ...clientOptions,
8621
+ retryNonIdempotentStatuses: [409]
8622
+ });
8623
+ return {
8624
+ async getProject(project) {
8625
+ const value = await api.json(`projects/${encodeURIComponent(project)}`, z.looseObject({
8626
+ id: z.union([z.number(), z.string()]).transform(String),
8627
+ path_with_namespace: z.string().min(1)
8628
+ }));
8629
+ return {
8630
+ id: value.id,
8631
+ path: value.path_with_namespace
8632
+ };
8633
+ },
8634
+ currentUser: () => api.json("user", userSchema),
8635
+ async loadChange(options) {
8636
+ const mergeRequest = await this.getMergeRequest(options.projectId, options.changeNumber);
8637
+ return {
8638
+ repository: {
8639
+ slug: options.projectPath,
8640
+ url: projectWebUrl(mergeRequest.web_url)
8641
+ },
8642
+ coordinates: {
8643
+ provider: "gitlab",
8644
+ projectId: String(mergeRequest.target_project_id),
8645
+ projectPath: options.projectPath
8646
+ },
8647
+ change: {
8648
+ number: mergeRequest.iid,
8649
+ title: mergeRequest.title,
8650
+ description: mergeRequest.description ?? "",
8651
+ url: mergeRequest.web_url,
8652
+ author: mergeRequest.author ? { login: mergeRequest.author.username } : void 0,
8653
+ base: {
8654
+ sha: mergeRequest.diff_refs.start_sha,
8655
+ ref: mergeRequest.target_branch
8656
+ },
8657
+ head: {
8658
+ sha: mergeRequest.diff_refs.head_sha,
8659
+ ref: mergeRequest.source_branch
8660
+ },
8661
+ isFork: mergeRequest.source_project_id !== mergeRequest.target_project_id,
8662
+ isDraft: mergeRequest.draft
8663
+ }
8664
+ };
8665
+ },
8666
+ async getMergeRequest(projectId, changeNumber) {
8667
+ const requestPath = `projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}`;
8668
+ for (let attempt = 0;; attempt += 1) {
8669
+ const response = await api.json(requestPath, mergeRequestResponseSchema);
8670
+ const prepared = mergeRequestSchema.safeParse(response);
8671
+ if (prepared.success) return prepared.data;
8672
+ if (attempt >= 4) throw new Error("GitLab merge request diff refs were not prepared after 5 attempts");
8673
+ await sleep(250 * 2 ** attempt);
8674
+ }
8675
+ },
8676
+ async getRepositoryPermission(projectId, username) {
8677
+ const user = (await api.json(`users?username=${encodeURIComponent(username)}`, z.array(userSchema))).find((candidate) => candidate.username === username);
8678
+ if (!user) return "none";
8679
+ try {
8680
+ return accessLevelPermission((await api.json(`projects/${encodeURIComponent(projectId)}/members/all/${user.id}`, memberSchema)).access_level);
8681
+ } catch (error) {
8682
+ if (error instanceof CodeHostHttpError && error.status === 404) return "none";
8683
+ throw error;
8684
+ }
8685
+ },
8686
+ listNotes: (projectId, changeNumber) => paginated(api, `projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/notes`, noteSchema),
8687
+ createNote: (projectId, changeNumber, body) => api.json(`projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/notes`, noteSchema, jsonRequest("POST", { body })),
8688
+ updateNote: (projectId, changeNumber, noteId, body) => api.json(`projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/notes/${encodeURIComponent(noteId)}`, noteSchema, jsonRequest("PUT", { body })),
8689
+ listDiscussions: (projectId, changeNumber) => paginated(api, `projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/discussions`, discussionSchema),
8690
+ getDiscussion: (projectId, changeNumber, discussionId) => api.json(`projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/discussions/${encodeURIComponent(discussionId)}`, discussionSchema),
8691
+ async findReplyParent(projectId, changeNumber, noteId, discussionId) {
8692
+ if (!discussionId) return void 0;
8693
+ const discussion = await this.getDiscussion(projectId, changeNumber, discussionId);
8694
+ if (!discussion.notes.some((note) => note.id === noteId) || discussion.notes[0]?.id === noteId) return;
8695
+ return discussion.notes[0]?.id;
8696
+ },
8697
+ createDiscussion: (projectId, changeNumber, body, position) => api.json(`projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/discussions`, discussionSchema, jsonRequest("POST", {
8698
+ body,
8699
+ position
8700
+ })),
8701
+ replyDiscussion: (projectId, changeNumber, discussionId, body) => api.json(`projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/discussions/${encodeURIComponent(discussionId)}/notes`, noteSchema, jsonRequest("POST", { body })),
8702
+ async resolveDiscussion(projectId, changeNumber, discussionId) {
8703
+ await api.json(`projects/${encodeURIComponent(projectId)}/merge_requests/${changeNumber}/discussions/${encodeURIComponent(discussionId)}`, discussionSchema, jsonRequest("PUT", { resolved: true }));
8704
+ },
8705
+ async setStatus(projectId, sha, name, state, description) {
8706
+ const status = await statusApi.json(`projects/${encodeURIComponent(projectId)}/statuses/${encodeURIComponent(sha)}`, statusSchema, jsonRequest("POST", {
8707
+ state: gitLabStatusState(state),
8708
+ name,
8709
+ description: boundedStatusDescription(description)
8710
+ }));
8711
+ return String(status.id ?? name);
8712
+ }
8713
+ };
8714
+ }
8715
+ const MAX_PAGINATION_PAGES = 100;
8716
+ async function paginated(client, path, schema) {
8717
+ const values = [];
8718
+ for (let page = 1; page <= MAX_PAGINATION_PAGES; page += 1) {
8719
+ const separator = path.includes("?") ? "&" : "?";
8720
+ const batch = await client.json(`${path}${separator}per_page=100&page=${page}`, z.array(schema));
8721
+ values.push(...batch);
8722
+ if (batch.length < 100) return values;
8723
+ }
8724
+ throw new Error(`GitLab pagination exceeded ${MAX_PAGINATION_PAGES} pages for ${path}`);
8725
+ }
8726
+ function jsonRequest(method, body) {
8727
+ return {
8728
+ method,
8729
+ headers: { "Content-Type": "application/json" },
8730
+ body: JSON.stringify(body)
8731
+ };
8732
+ }
8733
+ function accessLevelPermission(level) {
8734
+ if (level >= 50) return "admin";
8735
+ if (level >= 40) return "maintain";
8736
+ if (level >= 30) return "write";
8737
+ if (level >= 15) return "triage";
8738
+ if (level >= 10) return "read";
8739
+ return "none";
8740
+ }
8741
+ function boundedStatusDescription(description) {
8742
+ return description && description.length > 255 ? `${description.slice(0, 252).trimEnd()}...` : description;
8743
+ }
8744
+ function gitLabStatusState(state) {
8745
+ switch (state) {
8746
+ case "pending": return "pending";
8747
+ case "success": return "success";
8748
+ case "failure": return "failed";
8749
+ case "neutral": return "skipped";
8750
+ }
8751
+ }
8752
+ function projectWebUrl(mergeRequestUrl) {
8753
+ return mergeRequestUrl?.replace(/\/-\/merge_requests\/\d+$/, "");
8754
+ }
8755
+ //#endregion
8480
8756
  //#region src/hosts/gitlab/event.ts
8481
8757
  const projectSchema = z.looseObject({
8482
8758
  id: z.union([z.number(), z.string()]),
@@ -8723,8 +8999,17 @@ async function loadGitLabPriorReviewState(options) {
8723
8999
  const state = extractPriorReviewState(await loadGitLabPriorMainComment(options), options.change.change.number);
8724
9000
  if (!state) return void 0;
8725
9001
  const owner = await options.client.currentUser();
8726
- const bodies = discussionNotes(await options.client.listDiscussions(gitLabCoordinates$1(options.change).projectId, options.change.change.number)).filter((note) => note.author?.username === owner.username).map((note) => note.body);
8727
- return applyResolvedFindingMarkers(applyInlineFindingMarkers(state, bodies), bodies);
9002
+ const discussions = await options.client.listDiscussions(gitLabCoordinates$1(options.change).projectId, options.change.change.number);
9003
+ const bodies = discussionNotes(discussions).filter((note) => note.author?.username === owner.username).map((note) => note.body);
9004
+ return applyNativeThreadResolutions(applyResolvedFindingMarkers(applyInlineFindingMarkers(state, bodies), bodies), discussions.flatMap((discussion) => {
9005
+ const root = discussion.notes[0];
9006
+ const marker = root ? extractInlineFindingMarkerRecords([root.body])[0] : void 0;
9007
+ return root && marker && root.author?.username === owner.username ? [{
9008
+ findingId: marker.id,
9009
+ findingHeadSha: marker.head,
9010
+ resolved: root.resolved ?? false
9011
+ }] : [];
9012
+ }));
8728
9013
  }
8729
9014
  async function loadGitLabPriorMainComment(options) {
8730
9015
  const owner = await options.client.currentUser();
@@ -9980,6 +10265,6 @@ async function runHostRunCommandWithDependencies(options) {
9980
10265
  });
9981
10266
  }
9982
10267
  //#endregion
9983
- export { piReadOnlyToolNames as _, runInspectCommand as a, createGitHubHostAdapter as c, createGitLabClient as d, createBitbucketClient as f, piBuiltinToolNames as g, supportedOfficialInitAdapters as h, runInitCommand as i, PublicationError as l, azureOrganizationFromUrl as m, runHostRunCommand as n, runLocalReviewCommand as o, createAzureDevOpsClient as p, runHostRunCommandWithDependencies as r, runValidateCommand as s, runDryRunCommand as t, createKnownSecretRedactor as u, piRequiredCliFlags as v, piThinkingLevels as y };
10268
+ export { piReadOnlyToolNames as _, runInspectCommand as a, createGitLabClient as c, PublicationError as d, createAzureDevOpsClient as f, piBuiltinToolNames as g, supportedOfficialInitAdapters as h, runInitCommand as i, createGitHubHostAdapter as l, createKnownSecretRedactor as m, runHostRunCommand as n, runLocalReviewCommand as o, azureOrganizationFromUrl as p, runHostRunCommandWithDependencies as r, runValidateCommand as s, runDryRunCommand as t, createBitbucketClient as u, piRequiredCliFlags as v, piThinkingLevels as y };
9984
10269
 
9985
- //# sourceMappingURL=commands-46Q2ZDt0.mjs.map
10270
+ //# sourceMappingURL=commands-B9qNW4pk.mjs.map