github-issue-tower-defence-management 1.148.20 → 1.148.22

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.
@@ -197,7 +197,7 @@ export class DailySecurityScanUseCase {
197
197
  return [];
198
198
  }
199
199
 
200
- const scannedVulnerablePackages: ScannedVulnerablePackage[] = [];
200
+ const remoteUrlByRepositoryName = new Map<string, string>();
201
201
  for (const repositoryDirectory of repositoryDirectories) {
202
202
  const { stdout: remoteUrl, exitCode: remoteExitCode } =
203
203
  await this.localCommandRunner.runCommand('git', [
@@ -217,8 +217,21 @@ export class DailySecurityScanUseCase {
217
217
  if (!remoteMatch || remoteMatch[1] !== org) {
218
218
  continue;
219
219
  }
220
- const repositoryOrg = remoteMatch[1];
221
- const repositoryName = remoteMatch[2];
220
+ if (!remoteUrlByRepositoryName.has(remoteMatch[2])) {
221
+ remoteUrlByRepositoryName.set(remoteMatch[2], remoteUrl.trim());
222
+ }
223
+ }
224
+
225
+ const scannedVulnerablePackages: ScannedVulnerablePackage[] = [];
226
+ for (const [repositoryName, remoteUrl] of remoteUrlByRepositoryName) {
227
+ const repositoryOrg = org;
228
+ const checkoutDirectory = await this.checkoutDefaultBranch(
229
+ repositoryName,
230
+ remoteUrl,
231
+ );
232
+ if (checkoutDirectory === null) {
233
+ continue;
234
+ }
222
235
 
223
236
  const {
224
237
  stdout: scanOutput,
@@ -228,16 +241,20 @@ export class DailySecurityScanUseCase {
228
241
  'scan',
229
242
  'source',
230
243
  '-r',
231
- repositoryDirectory,
244
+ checkoutDirectory,
232
245
  '--format',
233
246
  'json',
234
247
  ]);
248
+ await this.localCommandRunner.runCommand('rm', [
249
+ '-rf',
250
+ checkoutDirectory,
251
+ ]);
235
252
  if (scanExitCode === 0) {
236
253
  continue;
237
254
  }
238
255
  if (scanExitCode !== 1) {
239
256
  console.error(
240
- `osv-scanner failed with exit code ${scanExitCode} for ${repositoryDirectory}: ${scanStderr}`,
257
+ `osv-scanner failed with exit code ${scanExitCode} for ${repositoryName}: ${scanStderr}`,
241
258
  );
242
259
  continue;
243
260
  }
@@ -278,6 +295,44 @@ export class DailySecurityScanUseCase {
278
295
  return scannedVulnerablePackages;
279
296
  };
280
297
 
298
+ private checkoutDefaultBranch = async (
299
+ repositoryName: string,
300
+ remoteUrl: string,
301
+ ): Promise<string | null> => {
302
+ const { stdout: checkoutDirectoryOutput, exitCode: checkoutDirectoryExit } =
303
+ await this.localCommandRunner.runCommand('mktemp', [
304
+ '-d',
305
+ '-t',
306
+ `tdpm-daily-security-scan-${repositoryName}-XXXXXX`,
307
+ ]);
308
+ const checkoutDirectory = checkoutDirectoryOutput.trim();
309
+ if (checkoutDirectoryExit !== 0 || checkoutDirectory.length === 0) {
310
+ console.error(
311
+ `Failed to create a checkout directory for ${repositoryName}`,
312
+ );
313
+ return null;
314
+ }
315
+ const { stderr: cloneStderr, exitCode: cloneExitCode } =
316
+ await this.localCommandRunner.runCommand('git', [
317
+ 'clone',
318
+ '--depth',
319
+ '1',
320
+ remoteUrl,
321
+ checkoutDirectory,
322
+ ]);
323
+ if (cloneExitCode !== 0) {
324
+ console.error(
325
+ `Failed to clone the default branch of ${repositoryName}: ${cloneStderr}`,
326
+ );
327
+ await this.localCommandRunner.runCommand('rm', [
328
+ '-rf',
329
+ checkoutDirectory,
330
+ ]);
331
+ return null;
332
+ }
333
+ return checkoutDirectory;
334
+ };
335
+
281
336
  private reportKevAdditions = async (
282
337
  org: string,
283
338
  manager: Member['name'],
@@ -420,6 +420,99 @@ describe('RevertOrphanedPreparationUseCase', () => {
420
420
  expect(mockIssueCommentRepository.createComment.mock.calls).toHaveLength(1);
421
421
  });
422
422
 
423
+ it('should advance an orphaned issue to Awaiting Quality Check when its own status comments were written after the report declaring no pull request is required', async () => {
424
+ const stuckIssue = createMockIssue({
425
+ url: 'https://github.com/user/repo/issues/10',
426
+ status: 'Preparation',
427
+ });
428
+ mockIssueRepository.getAllIssues.mockResolvedValue({
429
+ project: mockProject,
430
+ issues: [stuckIssue],
431
+ cacheUsed: false,
432
+ });
433
+ mockLocalCommandRunner.runCommand.mockResolvedValue({
434
+ stdout: '',
435
+ stderr: '',
436
+ exitCode: 1,
437
+ });
438
+ mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
439
+ {
440
+ author: 'agent-bot',
441
+ content:
442
+ 'From: :robot: agent report\n\n```json\n{ "pullRequestRequired": false, "nextStep": null }\n```\n',
443
+ createdAt: new Date(),
444
+ },
445
+ {
446
+ author: 'agent-bot',
447
+ content:
448
+ 'Auto Status Check: RETURNED_TO_AWAITING_WORKSPACE\nThe last report declared that this task needs no pull request.',
449
+ createdAt: new Date(),
450
+ },
451
+ {
452
+ author: 'agent-bot',
453
+ content: 'Auto Status Check: REJECTED\n- ORPHANED_PREPARATION',
454
+ createdAt: new Date(),
455
+ },
456
+ ]);
457
+ mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([]);
458
+
459
+ await useCase.run({
460
+ projectUrl: 'https://github.com/user/repo',
461
+ preparationProcessCheckCommand: 'pgrep -fa "claude-agent.*{URL}"',
462
+ thresholdForAutoReject: 3,
463
+ allowedIssueAuthors: ['agent-bot'],
464
+ });
465
+
466
+ expect(mockIssueRepository.findRelatedOpenPRs.mock.calls).toHaveLength(0);
467
+ expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(1);
468
+ expect(mockIssueRepository.updateStatus.mock.calls[0][2]).toBe('4');
469
+ expect(mockIssueCommentRepository.createComment.mock.calls).toHaveLength(0);
470
+ });
471
+
472
+ it('should keep rejecting an orphaned issue whose own status comments follow a report that declares nothing', async () => {
473
+ const stuckIssue = createMockIssue({
474
+ url: 'https://github.com/user/repo/issues/10',
475
+ status: 'Preparation',
476
+ });
477
+ mockIssueRepository.getAllIssues.mockResolvedValue({
478
+ project: mockProject,
479
+ issues: [stuckIssue],
480
+ cacheUsed: false,
481
+ });
482
+ mockLocalCommandRunner.runCommand.mockResolvedValue({
483
+ stdout: '',
484
+ stderr: '',
485
+ exitCode: 1,
486
+ });
487
+ mockIssueCommentRepository.getCommentsFromIssue.mockResolvedValue([
488
+ {
489
+ author: 'agent-bot',
490
+ content: 'From: :robot: agent report of an earlier session',
491
+ createdAt: new Date(),
492
+ },
493
+ {
494
+ author: 'agent-bot',
495
+ content: 'Auto Status Check: REJECTED\n- ORPHANED_PREPARATION',
496
+ createdAt: new Date(),
497
+ },
498
+ ]);
499
+ mockIssueRepository.findRelatedOpenPRs.mockResolvedValue([]);
500
+
501
+ await useCase.run({
502
+ projectUrl: 'https://github.com/user/repo',
503
+ preparationProcessCheckCommand: 'pgrep -fa "claude-agent.*{URL}"',
504
+ thresholdForAutoReject: 3,
505
+ allowedIssueAuthors: ['agent-bot'],
506
+ });
507
+
508
+ expect(mockIssueRepository.updateStatus.mock.calls).toHaveLength(1);
509
+ expect(mockIssueRepository.updateStatus.mock.calls[0][2]).toBe('1');
510
+ expect(mockIssueCommentRepository.createComment.mock.calls).toHaveLength(1);
511
+ expect(mockIssueCommentRepository.createComment.mock.calls[0][1]).toContain(
512
+ 'Auto Status Check: REJECTED',
513
+ );
514
+ });
515
+
423
516
  it('should advance orphaned issue with llm-agent label to Awaiting Quality Check without PR check', async () => {
424
517
  const stuckIssue = createMockIssue({
425
518
  url: 'https://github.com/user/repo/issues/10',
@@ -15,6 +15,7 @@ import {
15
15
  } from '../entities/WorkflowStatus';
16
16
  import { resolveLabelsNotRequiringPullRequest } from './resolveLabelsNotRequiringPullRequest';
17
17
  import { isPullRequestDeclaredUnnecessary } from './isPullRequestDeclaredUnnecessary';
18
+ import { dropTrailingAutoStatusCheckComments } from './autoStatusCheckComments';
18
19
  import { isAuthorAuthorizedForAutoStatusCheck } from './isAuthorAuthorizedForAutoStatusCheck';
19
20
  import {
20
21
  RETURNED_TO_AWAITING_WORKSPACE_MESSAGE,
@@ -186,16 +187,24 @@ export class RevertOrphanedPreparationUseCase {
186
187
  }
187
188
  const comments =
188
189
  await this.issueCommentRepository.getCommentsFromIssue(issue);
189
- const lastComment = comments[comments.length - 1];
190
- if (!lastComment || !lastComment.content.startsWith('From: :robot:')) {
191
- return { outcome: 'reject', comments };
192
- }
193
- if (this.reportBodyHasNextStep(lastComment.content)) {
194
- return { outcome: 'reject', comments };
195
- }
196
190
  const isTrustedAuthor = (author: string): boolean =>
197
191
  isAuthorAuthorizedForAutoStatusCheck(author, allowedIssueAuthors);
198
- if (isPullRequestDeclaredUnnecessary(comments, isTrustedAuthor)) {
192
+ const commentsBeforeOwnStatusComments = dropTrailingAutoStatusCheckComments(
193
+ comments,
194
+ isTrustedAuthor,
195
+ );
196
+ const lastReport =
197
+ commentsBeforeOwnStatusComments[
198
+ commentsBeforeOwnStatusComments.length - 1
199
+ ] ?? null;
200
+ if (
201
+ lastReport !== null &&
202
+ isPullRequestDeclaredUnnecessary(
203
+ commentsBeforeOwnStatusComments,
204
+ isTrustedAuthor,
205
+ ) &&
206
+ !this.reportBodyHasNextStep(lastReport.content)
207
+ ) {
199
208
  const alreadyReturnedToWorkspace = comments.some(
200
209
  (comment) =>
201
210
  isTrustedAuthor(comment.author) &&
@@ -211,6 +220,14 @@ export class RevertOrphanedPreparationUseCase {
211
220
  };
212
221
  }
213
222
 
223
+ const lastComment = comments[comments.length - 1];
224
+ if (!lastComment || !lastComment.content.startsWith('From: :robot:')) {
225
+ return { outcome: 'reject', comments };
226
+ }
227
+ if (this.reportBodyHasNextStep(lastComment.content)) {
228
+ return { outcome: 'reject', comments };
229
+ }
230
+
214
231
  const categoryLabels = issue.labels.filter((label) =>
215
232
  label.startsWith('category:'),
216
233
  );
@@ -0,0 +1,18 @@
1
+ export const AUTO_STATUS_CHECK_MESSAGE_HEAD = 'Auto Status Check:';
2
+
3
+ export const dropTrailingAutoStatusCheckComments = <
4
+ T extends { author: string; content: string },
5
+ >(
6
+ comments: T[],
7
+ isTrustedAuthor: (author: string) => boolean,
8
+ ): T[] => {
9
+ let endIndex = comments.length;
10
+ while (
11
+ endIndex > 0 &&
12
+ isTrustedAuthor(comments[endIndex - 1].author) &&
13
+ comments[endIndex - 1].content.startsWith(AUTO_STATUS_CHECK_MESSAGE_HEAD)
14
+ ) {
15
+ endIndex -= 1;
16
+ }
17
+ return comments.slice(0, endIndex);
18
+ };
@@ -20,6 +20,7 @@ export declare class DailySecurityScanUseCase {
20
20
  dailySecurityScan: DailySecurityScanConfig;
21
21
  }) => Promise<void>;
22
22
  private scanRepositories;
23
+ private checkoutDefaultBranch;
23
24
  private reportKevAdditions;
24
25
  }
25
26
  //# sourceMappingURL=DailySecurityScanUseCase.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"DailySecurityScanUseCase.d.ts","sourceRoot":"","sources":["../../../src/domain/usecases/DailySecurityScanUseCase.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAC7E,OAAO,EAAE,eAAe,EAAE,MAAM,sCAAsC,CAAC;AACvE,OAAO,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAC;AACrE,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAE5C,MAAM,MAAM,uBAAuB,GAAG;IACpC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAkHF,qBAAa,wBAAwB;IAEjC,QAAQ,CAAC,kBAAkB,EAAE,kBAAkB;IAC/C,QAAQ,CAAC,eAAe,EAAE,IAAI,CAC5B,eAAe,EACf,gBAAgB,GAAG,aAAa,GAAG,oBAAoB,CACxD;IACD,QAAQ,CAAC,cAAc,EAAE,cAAc;gBAL9B,kBAAkB,EAAE,kBAAkB,EACtC,eAAe,EAAE,IAAI,CAC5B,eAAe,EACf,gBAAgB,GAAG,aAAa,GAAG,oBAAoB,CACxD,EACQ,cAAc,EAAE,cAAc;IAGzC,GAAG,GAAU,OAAO;QAClB,WAAW,EAAE,IAAI,EAAE,CAAC;QACpB,GAAG,EAAE,MAAM,CAAC;QACZ,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;QACxB,iBAAiB,EAAE,uBAAuB,CAAC;KAC5C,KAAG,OAAO,CAAC,IAAI,CAAC,CA2Bf;IAEF,OAAO,CAAC,gBAAgB,CA8GtB;IAEF,OAAO,CAAC,kBAAkB,CAwDxB;CACH"}
1
+ {"version":3,"file":"DailySecurityScanUseCase.d.ts","sourceRoot":"","sources":["../../../src/domain/usecases/DailySecurityScanUseCase.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAC7E,OAAO,EAAE,eAAe,EAAE,MAAM,sCAAsC,CAAC;AACvE,OAAO,EAAE,cAAc,EAAE,MAAM,qCAAqC,CAAC;AACrE,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAE5C,MAAM,MAAM,uBAAuB,GAAG;IACpC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,aAAa,EAAE,MAAM,CAAC;IACtB,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAkHF,qBAAa,wBAAwB;IAEjC,QAAQ,CAAC,kBAAkB,EAAE,kBAAkB;IAC/C,QAAQ,CAAC,eAAe,EAAE,IAAI,CAC5B,eAAe,EACf,gBAAgB,GAAG,aAAa,GAAG,oBAAoB,CACxD;IACD,QAAQ,CAAC,cAAc,EAAE,cAAc;gBAL9B,kBAAkB,EAAE,kBAAkB,EACtC,eAAe,EAAE,IAAI,CAC5B,eAAe,EACf,gBAAgB,GAAG,aAAa,GAAG,oBAAoB,CACxD,EACQ,cAAc,EAAE,cAAc;IAGzC,GAAG,GAAU,OAAO;QAClB,WAAW,EAAE,IAAI,EAAE,CAAC;QACpB,GAAG,EAAE,MAAM,CAAC;QACZ,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;QACxB,iBAAiB,EAAE,uBAAuB,CAAC;KAC5C,KAAG,OAAO,CAAC,IAAI,CAAC,CA2Bf;IAEF,OAAO,CAAC,gBAAgB,CA+HtB;IAEF,OAAO,CAAC,qBAAqB,CAoC3B;IAEF,OAAO,CAAC,kBAAkB,CAwDxB;CACH"}
@@ -1 +1 @@
1
- {"version":3,"file":"RevertOrphanedPreparationUseCase.d.ts","sourceRoot":"","sources":["../../../src/domain/usecases/RevertOrphanedPreparationUseCase.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EAEhB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EAAE,sBAAsB,EAAE,MAAM,6CAA6C,CAAC;AACrF,OAAO,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAC;AAC3E,OAAO,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAsB7E,qBAAa,gCAAgC;IAEzC,QAAQ,CAAC,iBAAiB,EAAE,IAAI,CAC9B,iBAAiB,EACjB,oBAAoB,GAAG,YAAY,CACpC;IACD,QAAQ,CAAC,eAAe,EAAE,IAAI,CAC5B,eAAe,EACb,cAAc,GACd,cAAc,GACd,oBAAoB,GACpB,oBAAoB,CACvB;IACD,QAAQ,CAAC,sBAAsB,EAAE,IAAI,CACnC,sBAAsB,EACtB,sBAAsB,GAAG,eAAe,CACzC;IACD,QAAQ,CAAC,kBAAkB,EAAE,kBAAkB;gBAftC,iBAAiB,EAAE,IAAI,CAC9B,iBAAiB,EACjB,oBAAoB,GAAG,YAAY,CACpC,EACQ,eAAe,EAAE,IAAI,CAC5B,eAAe,EACb,cAAc,GACd,cAAc,GACd,oBAAoB,GACpB,oBAAoB,CACvB,EACQ,sBAAsB,EAAE,IAAI,CACnC,sBAAsB,EACtB,sBAAsB,GAAG,eAAe,CACzC,EACQ,kBAAkB,EAAE,kBAAkB;IAGjD,GAAG,GAAU,QAAQ;QACnB,UAAU,EAAE,MAAM,CAAC;QACnB,8BAA8B,EAAE,MAAM,CAAC;QACvC,sBAAsB,EAAE,MAAM,CAAC;QAC/B,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAC5B,0BAA0B,CAAC,EAAE,MAAM,CAAC;QACpC,0BAA0B,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC3C,oBAAoB,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;QACvC,6BAA6B,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;QAChD,mBAAmB,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;KACvC,KAAG,OAAO,CAAC,IAAI,CAAC,CAmHf;IAEF,OAAO,CAAC,eAAe,CAwErB;IAEF,OAAO,CAAC,uBAAuB,CAQ7B;IAEF,OAAO,CAAC,qBAAqB,CAuB3B;IAEF,OAAO,CAAC,eAAe,CA6BrB;IAEF,OAAO,CAAC,YAAY,CA+BlB;CACH"}
1
+ {"version":3,"file":"RevertOrphanedPreparationUseCase.d.ts","sourceRoot":"","sources":["../../../src/domain/usecases/RevertOrphanedPreparationUseCase.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,eAAe,EAEhB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EAAE,sBAAsB,EAAE,MAAM,6CAA6C,CAAC;AACrF,OAAO,EAAE,iBAAiB,EAAE,MAAM,wCAAwC,CAAC;AAC3E,OAAO,EAAE,kBAAkB,EAAE,MAAM,yCAAyC,CAAC;AAuB7E,qBAAa,gCAAgC;IAEzC,QAAQ,CAAC,iBAAiB,EAAE,IAAI,CAC9B,iBAAiB,EACjB,oBAAoB,GAAG,YAAY,CACpC;IACD,QAAQ,CAAC,eAAe,EAAE,IAAI,CAC5B,eAAe,EACb,cAAc,GACd,cAAc,GACd,oBAAoB,GACpB,oBAAoB,CACvB;IACD,QAAQ,CAAC,sBAAsB,EAAE,IAAI,CACnC,sBAAsB,EACtB,sBAAsB,GAAG,eAAe,CACzC;IACD,QAAQ,CAAC,kBAAkB,EAAE,kBAAkB;gBAftC,iBAAiB,EAAE,IAAI,CAC9B,iBAAiB,EACjB,oBAAoB,GAAG,YAAY,CACpC,EACQ,eAAe,EAAE,IAAI,CAC5B,eAAe,EACb,cAAc,GACd,cAAc,GACd,oBAAoB,GACpB,oBAAoB,CACvB,EACQ,sBAAsB,EAAE,IAAI,CACnC,sBAAsB,EACtB,sBAAsB,GAAG,eAAe,CACzC,EACQ,kBAAkB,EAAE,kBAAkB;IAGjD,GAAG,GAAU,QAAQ;QACnB,UAAU,EAAE,MAAM,CAAC;QACnB,8BAA8B,EAAE,MAAM,CAAC;QACvC,sBAAsB,EAAE,MAAM,CAAC;QAC/B,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAC5B,0BAA0B,CAAC,EAAE,MAAM,CAAC;QACpC,0BAA0B,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC3C,oBAAoB,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;QACvC,6BAA6B,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;QAChD,mBAAmB,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;KACvC,KAAG,OAAO,CAAC,IAAI,CAAC,CAmHf;IAEF,OAAO,CAAC,eAAe,CAwFrB;IAEF,OAAO,CAAC,uBAAuB,CAQ7B;IAEF,OAAO,CAAC,qBAAqB,CAuB3B;IAEF,OAAO,CAAC,eAAe,CA6BrB;IAEF,OAAO,CAAC,YAAY,CA+BlB;CACH"}
@@ -0,0 +1,6 @@
1
+ export declare const AUTO_STATUS_CHECK_MESSAGE_HEAD = "Auto Status Check:";
2
+ export declare const dropTrailingAutoStatusCheckComments: <T extends {
3
+ author: string;
4
+ content: string;
5
+ }>(comments: T[], isTrustedAuthor: (author: string) => boolean) => T[];
6
+ //# sourceMappingURL=autoStatusCheckComments.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"autoStatusCheckComments.d.ts","sourceRoot":"","sources":["../../../src/domain/usecases/autoStatusCheckComments.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,8BAA8B,uBAAuB,CAAC;AAEnE,eAAO,MAAM,mCAAmC,GAC9C,CAAC,SAAS;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,EAE7C,UAAU,CAAC,EAAE,EACb,iBAAiB,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,KAC3C,CAAC,EAUH,CAAC"}