agent-scenario-loop 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/app/profile-session.ts +263 -17
  2. package/dist/core/artifact-contract.d.ts +6 -4
  3. package/dist/core/artifact-contract.js +164 -15
  4. package/dist/core/artifact-layout.d.ts +2 -0
  5. package/dist/core/artifact-layout.js +2 -0
  6. package/dist/core/planner.js +4 -3
  7. package/dist/core/schema-validator.d.ts +1 -0
  8. package/dist/core/schema-validator.js +1 -0
  9. package/dist/runner/android-adb-driver.d.ts +7 -2
  10. package/dist/runner/android-adb-driver.js +7 -1
  11. package/dist/runner/android-adb.d.ts +40 -5
  12. package/dist/runner/android-adb.js +1046 -664
  13. package/dist/runner/ios-simctl.d.ts +1 -0
  14. package/dist/runner/ios-simctl.js +1 -0
  15. package/dist/runner/profile-android.d.ts +11 -1
  16. package/dist/runner/profile-android.js +266 -25
  17. package/dist/runner/profile-ios.d.ts +3 -2
  18. package/dist/runner/profile-ios.js +252 -22
  19. package/dist/runner/profile-mobile.d.ts +63 -4
  20. package/dist/runner/profile-mobile.js +1002 -20
  21. package/dist/runner/validate-project.js +3 -0
  22. package/dist/scripts/consumer-rehearsal.d.ts +127 -0
  23. package/dist/scripts/consumer-rehearsal.js +774 -0
  24. package/dist/scripts/downstream-local-package-gate.d.ts +2 -0
  25. package/dist/scripts/downstream-local-package-gate.js +264 -0
  26. package/dist/scripts/package-smoke.d.ts +104 -0
  27. package/dist/scripts/package-smoke.js +2304 -0
  28. package/dist/scripts/release-check.d.ts +47 -0
  29. package/dist/scripts/release-check.js +117 -0
  30. package/dist/scripts/release-readiness.d.ts +2 -0
  31. package/dist/scripts/release-readiness.js +539 -0
  32. package/docs/adapters.md +3 -1
  33. package/docs/api.md +2 -2
  34. package/docs/authoring.md +34 -2
  35. package/docs/consumer-rehearsal.md +33 -1
  36. package/docs/contracts.md +16 -2
  37. package/docs/live-proofs.md +12 -4
  38. package/examples/mobile-app/runner-manifests/evidence-provider.json +3 -3
  39. package/examples/mobile-app/scripts/asl-capture-profiler-provider.mjs +25 -0
  40. package/examples/runners/README.md +3 -3
  41. package/examples/runners/axe-accessibility-provider.json +2 -2
  42. package/examples/runners/script-accessibility-provider.json +2 -2
  43. package/examples/runners/script-memory-provider.json +2 -2
  44. package/examples/runners/script-network-provider.json +2 -2
  45. package/examples/runners/script-profiler-provider.json +2 -2
  46. package/package.json +12 -4
  47. package/schemas/manifest.schema.json +73 -3
  48. package/schemas/profiler.schema.json +243 -0
  49. package/schemas/runner-capabilities.schema.json +8 -2
  50. package/schemas/scenario.schema.json +18 -2
  51. package/templates/evidence-provider.json +3 -3
  52. package/templates/scripts/asl-capture-profiler-provider.mjs +20 -0
@@ -21,6 +21,7 @@ export type ProfileSessionCommand = {
21
21
  source?: 'deeplink' | 'storage';
22
22
  timestamp: number;
23
23
  waitForMilestone?: string;
24
+ waitMs?: number;
24
25
  waitTimeoutMs?: number;
25
26
  };
26
27
 
@@ -67,6 +68,7 @@ type StoredProfileSessionEntry = {
67
68
  scenario: string;
68
69
  runId: string;
69
70
  timestamp: number;
71
+ atMs?: number;
70
72
  startedAt?: number;
71
73
  stoppedAt?: number;
72
74
  command?: string;
@@ -79,10 +81,22 @@ type StoredProfileSessionEntry = {
79
81
  source?: 'deeplink' | 'storage';
80
82
  status?: 'received' | 'queued' | 'delivered' | 'completed' | 'skipped';
81
83
  waitForMilestone?: string;
84
+ waitMs?: number;
82
85
  waitTimeoutMs?: number;
83
86
  };
84
87
 
85
88
  type StoredProfileSignals = Record<ProfileSignalKind, Record<string, unknown>>;
89
+ type ProfileCommandMilestoneGate = {
90
+ commandId?: string;
91
+ id: string;
92
+ milestone: string;
93
+ queueId?: string;
94
+ runId?: string;
95
+ scenario?: string;
96
+ sequence?: number;
97
+ timeoutId?: ReturnType<typeof setTimeout>;
98
+ waitMs?: number;
99
+ };
86
100
 
87
101
  const INITIAL_STATE: ProfileSessionState = {
88
102
  active: false,
@@ -111,9 +125,15 @@ const listeners = new Set<() => void>();
111
125
  const profileCommandListeners = new Set<(command: ProfileSessionCommand) => void>();
112
126
  const profileCommandTargetHandlers = new Map<string, () => void>();
113
127
  const pendingProfileCommands: ProfileSessionCommand[] = [];
128
+ const sequencedProfileCommands: ProfileSessionCommand[] = [];
129
+ const observedProfileEvents: StoredProfileEvent[] = [];
114
130
  const processedProfileCommandIds = new Set<string>();
115
131
  let lastProfileCommandSignature: string | null = null;
116
132
  let lastProfileCommandTimestamp = 0;
133
+ let profileCommandMilestoneGate: ProfileCommandMilestoneGate | null = null;
134
+ let profileCommandProcessingScheduled = false;
135
+ let profileCommandProcessingTimeoutId: ReturnType<typeof setTimeout> | null = null;
136
+ let profileCommandProcessingAvailableAt = 0;
117
137
 
118
138
  function writeProfileLog(line: string) {
119
139
  if (Platform.OS === 'ios') {
@@ -205,6 +225,7 @@ function appendStoredProfileEvent(event: StoredProfileEvent) {
205
225
  }
206
226
 
207
227
  function resetStoredProfileArtifacts() {
228
+ observedProfileEvents.length = 0;
208
229
  queueProfileStorageMutation(async () => {
209
230
  await Promise.all([
210
231
  AsyncStorage.removeItem(PROFILE_EVENT_STORAGE_KEY),
@@ -216,6 +237,9 @@ function resetStoredProfileArtifacts() {
216
237
 
217
238
  function clearPendingProfileCommands() {
218
239
  pendingProfileCommands.length = 0;
240
+ sequencedProfileCommands.length = 0;
241
+ clearProfileCommandMilestoneGate();
242
+ clearProfileCommandProcessingSchedule();
219
243
  queueProfileStorageMutation(async () => {
220
244
  await AsyncStorage.removeItem(PROFILE_COMMAND_STORAGE_KEY);
221
245
  });
@@ -299,7 +323,21 @@ export function isProfileSessionFresh(
299
323
  }
300
324
 
301
325
  function logProfileSession(kind: 'start' | 'stop' | 'command', payload: Record<string, unknown>) {
302
- writeProfileLog(buildLogLine('profile-session', { kind, ...payload }));
326
+ const timestamp = Date.now();
327
+ const sessionStartedAt = readProfileSessionStartedAt(profileSessionState);
328
+ const atMs =
329
+ typeof payload.atMs === 'number' && Number.isFinite(payload.atMs)
330
+ ? payload.atMs
331
+ : sessionStartedAt !== null
332
+ ? Math.max(0, timestamp - sessionStartedAt)
333
+ : undefined;
334
+ const logPayload = {
335
+ kind,
336
+ ...payload,
337
+ timestamp,
338
+ ...(atMs !== undefined ? { atMs } : {}),
339
+ };
340
+ writeProfileLog(buildLogLine('profile-session', logPayload));
303
341
 
304
342
  const scenario = typeof payload.scenario === 'string' ? payload.scenario : null;
305
343
  const runId = typeof payload.runId === 'string' ? payload.runId : null;
@@ -307,12 +345,12 @@ function logProfileSession(kind: 'start' | 'stop' | 'command', payload: Record<s
307
345
  return;
308
346
  }
309
347
 
310
- const timestamp = Date.now();
311
348
  const entry: StoredProfileSessionEntry = {
312
349
  kind,
313
350
  scenario,
314
351
  runId,
315
352
  timestamp,
353
+ ...(atMs !== undefined ? { atMs } : {}),
316
354
  };
317
355
 
318
356
  if (kind === 'start' && typeof payload.startedAt === 'number') {
@@ -362,6 +400,9 @@ function logProfileSession(kind: 'start' | 'stop' | 'command', payload: Record<s
362
400
  if (typeof payload.waitForMilestone === 'string') {
363
401
  entry.waitForMilestone = payload.waitForMilestone;
364
402
  }
403
+ if (typeof payload.waitMs === 'number') {
404
+ entry.waitMs = payload.waitMs;
405
+ }
365
406
  if (typeof payload.waitTimeoutMs === 'number') {
366
407
  entry.waitTimeoutMs = payload.waitTimeoutMs;
367
408
  }
@@ -379,6 +420,7 @@ function getProfileSessionRoute(url: string): {
379
420
  queueId?: string;
380
421
  sequence?: number;
381
422
  waitForMilestone?: string;
423
+ waitMs?: number;
382
424
  waitTimeoutMs?: number;
383
425
  } | null {
384
426
  const parsed = ExpoLinking.parse(url);
@@ -415,8 +457,12 @@ function getProfileSessionRoute(url: string): {
415
457
  typeof parsed.queryParams?.waitTimeoutMs === 'string' && Number.isInteger(Number(parsed.queryParams.waitTimeoutMs))
416
458
  ? Number(parsed.queryParams.waitTimeoutMs)
417
459
  : undefined;
460
+ const waitMs =
461
+ typeof parsed.queryParams?.waitMs === 'string' && Number.isInteger(Number(parsed.queryParams.waitMs))
462
+ ? Number(parsed.queryParams.waitMs)
463
+ : undefined;
418
464
 
419
- return { action, scenario, runId, command, commandId, queueId, sequence, waitForMilestone, waitTimeoutMs };
465
+ return { action, scenario, runId, command, commandId, queueId, sequence, waitForMilestone, waitMs, waitTimeoutMs };
420
466
  }
421
467
 
422
468
  function queuePendingProfileCommand(command: ProfileSessionCommand) {
@@ -508,6 +554,132 @@ function markProfileCommandIdProcessed(command: ProfileSessionCommand) {
508
554
  }
509
555
  }
510
556
 
557
+ function compareProfileCommands(left: ProfileSessionCommand, right: ProfileSessionCommand): number {
558
+ const leftSequence = typeof left.sequence === 'number' ? left.sequence : Number.POSITIVE_INFINITY;
559
+ const rightSequence = typeof right.sequence === 'number' ? right.sequence : Number.POSITIVE_INFINITY;
560
+ if (leftSequence !== rightSequence) {
561
+ return leftSequence - rightSequence;
562
+ }
563
+
564
+ return left.timestamp - right.timestamp;
565
+ }
566
+
567
+ function shouldQueueProfileCommand(command: ProfileSessionCommand): boolean {
568
+ return !hasProcessedProfileCommandId(command) &&
569
+ !sequencedProfileCommands.some((queuedCommand) => queuedCommand.id === command.id);
570
+ }
571
+
572
+ function buildProfileCommandMilestoneGate(command: ProfileSessionCommand): ProfileCommandMilestoneGate | null {
573
+ if (typeof command.waitForMilestone !== 'string' || command.waitForMilestone.length === 0) {
574
+ return null;
575
+ }
576
+
577
+ return {
578
+ id: command.id,
579
+ milestone: command.waitForMilestone,
580
+ ...(typeof command.commandId === 'string' ? { commandId: command.commandId } : {}),
581
+ ...(typeof command.queueId === 'string' ? { queueId: command.queueId } : {}),
582
+ ...(typeof command.runId === 'string' ? { runId: command.runId } : {}),
583
+ ...(typeof command.scenario === 'string' ? { scenario: command.scenario } : {}),
584
+ ...(typeof command.sequence === 'number' ? { sequence: command.sequence } : {}),
585
+ ...(typeof command.waitMs === 'number' && command.waitMs > 0 ? { waitMs: command.waitMs } : {}),
586
+ };
587
+ }
588
+
589
+ function hasObservedProfileCommandMilestone(command: ProfileSessionCommand): boolean {
590
+ if (typeof command.waitForMilestone !== 'string' || command.waitForMilestone.length === 0) {
591
+ return false;
592
+ }
593
+ if (typeof command.sequence === 'number' && command.sequence > 1) {
594
+ return false;
595
+ }
596
+
597
+ return observedProfileEvents.some((eventPayload) => (
598
+ eventPayload.event === command.waitForMilestone &&
599
+ (!command.runId || eventPayload.runId === command.runId) &&
600
+ (!command.scenario || eventPayload.scenario === command.scenario)
601
+ ));
602
+ }
603
+
604
+ function clearProfileCommandMilestoneGate() {
605
+ if (profileCommandMilestoneGate?.timeoutId) {
606
+ clearTimeout(profileCommandMilestoneGate.timeoutId);
607
+ }
608
+ profileCommandMilestoneGate = null;
609
+ }
610
+
611
+ function clearProfileCommandProcessingSchedule() {
612
+ if (profileCommandProcessingTimeoutId) {
613
+ clearTimeout(profileCommandProcessingTimeoutId);
614
+ }
615
+ profileCommandProcessingTimeoutId = null;
616
+ profileCommandProcessingScheduled = false;
617
+ profileCommandProcessingAvailableAt = 0;
618
+ }
619
+
620
+ function startProfileCommandMilestoneTimeout(command: ProfileSessionCommand) {
621
+ if (
622
+ !profileCommandMilestoneGate ||
623
+ typeof command.waitTimeoutMs !== 'number' ||
624
+ command.waitTimeoutMs <= 0
625
+ ) {
626
+ return;
627
+ }
628
+
629
+ profileCommandMilestoneGate.timeoutId = setTimeout(() => {
630
+ if (!profileCommandMilestoneGate || profileCommandMilestoneGate.id !== command.id) {
631
+ return;
632
+ }
633
+
634
+ logProfileSession('command', {
635
+ ...command,
636
+ status: 'skipped',
637
+ reason: 'wait-for-milestone-timeout',
638
+ });
639
+ clearProfileCommandMilestoneGate();
640
+ processSequencedProfileCommands();
641
+ }, command.waitTimeoutMs);
642
+ }
643
+
644
+ function scheduleProfileCommandProcessing(waitMs = 0) {
645
+ const availableAt = waitMs > 0 ? Date.now() + waitMs : 0;
646
+ if (availableAt > profileCommandProcessingAvailableAt) {
647
+ profileCommandProcessingAvailableAt = availableAt;
648
+ }
649
+
650
+ const delayMs = Math.max(0, profileCommandProcessingAvailableAt - Date.now());
651
+ if (profileCommandProcessingTimeoutId) {
652
+ clearTimeout(profileCommandProcessingTimeoutId);
653
+ profileCommandProcessingTimeoutId = null;
654
+ }
655
+
656
+ profileCommandProcessingScheduled = true;
657
+ const run = () => {
658
+ profileCommandProcessingTimeoutId = null;
659
+ const remainingMs = Math.max(0, profileCommandProcessingAvailableAt - Date.now());
660
+ if (remainingMs > 0) {
661
+ profileCommandProcessingScheduled = false;
662
+ scheduleProfileCommandProcessing(remainingMs);
663
+ return;
664
+ }
665
+ profileCommandProcessingAvailableAt = 0;
666
+ profileCommandProcessingScheduled = false;
667
+ processSequencedProfileCommands();
668
+ };
669
+
670
+ if (delayMs > 0) {
671
+ profileCommandProcessingTimeoutId = setTimeout(run, delayMs);
672
+ return;
673
+ }
674
+
675
+ if (typeof queueMicrotask === 'function') {
676
+ queueMicrotask(run);
677
+ return;
678
+ }
679
+
680
+ Promise.resolve().then(run);
681
+ }
682
+
511
683
  function notifyProfileCommandListeners(command: ProfileSessionCommand) {
512
684
  const commandTimestamp = Number.isFinite(command.timestamp) ? command.timestamp : Date.now();
513
685
  if (shouldSkipProfileCommandForDuplicateWindow(command, commandTimestamp)) {
@@ -552,6 +724,82 @@ function notifyProfileCommandListeners(command: ProfileSessionCommand) {
552
724
  });
553
725
  }
554
726
 
727
+ function processSequencedProfileCommands() {
728
+ if (profileCommandProcessingScheduled) {
729
+ return;
730
+ }
731
+ const remainingMs = Math.max(0, profileCommandProcessingAvailableAt - Date.now());
732
+ if (remainingMs > 0) {
733
+ scheduleProfileCommandProcessing(remainingMs);
734
+ return;
735
+ }
736
+ if (profileCommandMilestoneGate) {
737
+ return;
738
+ }
739
+
740
+ while (sequencedProfileCommands.length > 0) {
741
+ const command = sequencedProfileCommands.shift();
742
+ if (!command || hasProcessedProfileCommandId(command)) {
743
+ continue;
744
+ }
745
+
746
+ markProfileCommandIdProcessed(command);
747
+ const nextGate = hasObservedProfileCommandMilestone(command)
748
+ ? null
749
+ : buildProfileCommandMilestoneGate(command);
750
+ profileCommandMilestoneGate = nextGate;
751
+ logProfileSession('command', {
752
+ ...command,
753
+ status: 'received',
754
+ });
755
+ startProfileCommandMilestoneTimeout(command);
756
+ notifyProfileCommandListeners(command);
757
+
758
+ if (profileCommandMilestoneGate) {
759
+ return;
760
+ }
761
+ if (typeof command.waitMs === 'number' && command.waitMs > 0) {
762
+ scheduleProfileCommandProcessing(command.waitMs);
763
+ return;
764
+ }
765
+ }
766
+ }
767
+
768
+ function enqueueSequencedProfileCommands(commands: ProfileSessionCommand[]) {
769
+ const nextCommands = commands.filter(shouldQueueProfileCommand);
770
+ if (nextCommands.length === 0) {
771
+ return;
772
+ }
773
+
774
+ sequencedProfileCommands.push(...nextCommands);
775
+ sequencedProfileCommands.sort(compareProfileCommands);
776
+ processSequencedProfileCommands();
777
+ }
778
+
779
+ function releaseProfileCommandMilestoneGate(eventPayload: StoredProfileEvent) {
780
+ if (!profileCommandMilestoneGate || profileCommandMilestoneGate.milestone !== eventPayload.event) {
781
+ return;
782
+ }
783
+ if (
784
+ profileCommandMilestoneGate.runId &&
785
+ profileCommandMilestoneGate.runId !== eventPayload.runId
786
+ ) {
787
+ return;
788
+ }
789
+ if (
790
+ profileCommandMilestoneGate.scenario &&
791
+ profileCommandMilestoneGate.scenario !== eventPayload.scenario
792
+ ) {
793
+ return;
794
+ }
795
+
796
+ const waitMs = typeof profileCommandMilestoneGate.waitMs === 'number'
797
+ ? profileCommandMilestoneGate.waitMs
798
+ : 0;
799
+ clearProfileCommandMilestoneGate();
800
+ scheduleProfileCommandProcessing(waitMs);
801
+ }
802
+
555
803
  function flushPendingProfileCommands(listener: (command: ProfileSessionCommand) => void) {
556
804
  if (pendingProfileCommands.length === 0) {
557
805
  return;
@@ -649,13 +897,10 @@ export function applyProfileSessionUrl(url: string | null | undefined): boolean
649
897
  source: 'deeplink' as const,
650
898
  timestamp,
651
899
  ...(route.waitForMilestone ? { waitForMilestone: route.waitForMilestone } : {}),
900
+ ...(typeof route.waitMs === 'number' ? { waitMs: route.waitMs } : {}),
652
901
  ...(typeof route.waitTimeoutMs === 'number' ? { waitTimeoutMs: route.waitTimeoutMs } : {}),
653
902
  };
654
- logProfileSession('command', {
655
- ...command,
656
- status: 'received',
657
- });
658
- notifyProfileCommandListeners(command);
903
+ enqueueSequencedProfileCommands([command]);
659
904
  return true;
660
905
  }
661
906
 
@@ -698,7 +943,12 @@ export function emitProfileEvent(event: string, metadata?: ProfileEventMetadata)
698
943
  };
699
944
 
700
945
  writeProfileLog(buildLogLine('profile-event', eventPayload));
946
+ observedProfileEvents.push(eventPayload);
947
+ while (observedProfileEvents.length > MAX_STORED_PROFILE_EVENTS) {
948
+ observedProfileEvents.shift();
949
+ }
701
950
  appendStoredProfileEvent(eventPayload);
951
+ releaseProfileCommandMilestoneGate(eventPayload);
702
952
  }
703
953
 
704
954
  /**
@@ -838,7 +1088,7 @@ export function useProfileSessionBootstrap(): void {
838
1088
  return;
839
1089
  }
840
1090
 
841
- clearPendingProfileCommands();
1091
+ const nextCommands: ProfileSessionCommand[] = [];
842
1092
  for (const command of storedCommands) {
843
1093
  if (
844
1094
  !command ||
@@ -851,8 +1101,7 @@ export function useProfileSessionBootstrap(): void {
851
1101
 
852
1102
  if (
853
1103
  command.scenario !== activeSession.scenario ||
854
- command.runId !== activeSession.runId ||
855
- (typeof activeSession.startedAt === 'number' && command.timestamp < activeSession.startedAt)
1104
+ command.runId !== activeSession.runId
856
1105
  ) {
857
1106
  continue;
858
1107
  }
@@ -866,13 +1115,10 @@ export function useProfileSessionBootstrap(): void {
866
1115
  continue;
867
1116
  }
868
1117
 
869
- markProfileCommandIdProcessed(storageCommand);
870
- logProfileSession('command', {
871
- ...storageCommand,
872
- status: 'received',
873
- });
874
- notifyProfileCommandListeners(storageCommand);
1118
+ nextCommands.push(storageCommand);
875
1119
  }
1120
+
1121
+ enqueueSequencedProfileCommands(nextCommands);
876
1122
  };
877
1123
 
878
1124
  syncStoredProfileState()
@@ -56,10 +56,10 @@ declare function extractProfileSessionEntries(logText: string, filters?: {
56
56
  /**
57
57
  * Builds timing metrics from app-emitted profile events.
58
58
  *
59
- * @param {{scenario: string, runId: string, events: Record<string, unknown>[], expectedIterations: number, timeoutCount?: number, artifacts?: Record<string, unknown>, cycleEventNames?: Record<string, string> | null, budgets?: Record<string, unknown> | null}} options
59
+ * @param {{scenario: string, runId: string, events: Record<string, unknown>[], expectedIterations: number, timeoutCount?: number, artifacts?: Record<string, unknown>, cycleEventNames?: Record<string, string> | null, milestoneEventsPerIteration?: number, budgets?: Record<string, unknown> | null}} options
60
60
  * @returns {Record<string, unknown>}
61
61
  */
62
- declare function buildMetricsFromProfileEvents({ scenario, runId, events, expectedIterations, timeoutCount, artifacts, cycleEventNames, budgets, }: {
62
+ declare function buildMetricsFromProfileEvents({ scenario, runId, events, expectedIterations, timeoutCount, artifacts, cycleEventNames, milestoneEventsPerIteration, budgets, }: {
63
63
  scenario: string;
64
64
  runId: string;
65
65
  events: ProfileEvent[];
@@ -67,17 +67,19 @@ declare function buildMetricsFromProfileEvents({ scenario, runId, events, expect
67
67
  timeoutCount?: number;
68
68
  artifacts?: ArtifactRecord;
69
69
  cycleEventNames?: ArtifactRecord | null;
70
+ milestoneEventsPerIteration?: number;
70
71
  budgets?: ArtifactRecord | null;
71
72
  }): ArtifactRecord;
72
73
  /**
73
74
  * Evaluates configured profile budgets against generated metrics.
74
75
  *
75
- * @param {{metrics: Record<string, unknown>, budgets?: Record<string, unknown> | null}} options
76
+ * @param {{metrics: Record<string, unknown>, budgets?: Record<string, unknown> | null, extraChecks?: BudgetCheck[]}} options
76
77
  * @returns {Record<string, unknown> | null}
77
78
  */
78
- declare function evaluateProfileBudgets({ metrics, budgets }: {
79
+ declare function evaluateProfileBudgets({ metrics, budgets, extraChecks, }: {
79
80
  metrics: ArtifactRecord;
80
81
  budgets?: ArtifactRecord | null;
82
+ extraChecks?: BudgetCheck[];
81
83
  }): ArtifactRecord | null;
82
84
  /**
83
85
  * Recursively sorts object keys and array values for stable JSON artifacts.