rea-agents 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/README.md +78 -32
  2. package/bridge/ghidra/ReaGhidraBridge.java +96 -11
  3. package/dist/application/ArtifactExtraction.js +1 -1
  4. package/dist/application/ArtifactInventory/classify.js +42 -0
  5. package/dist/application/ArtifactInventory/hash.js +43 -0
  6. package/dist/application/ArtifactInventory/reader.js +41 -0
  7. package/dist/application/ArtifactInventory/scanCanonical.js +155 -0
  8. package/dist/application/ArtifactInventory/scanReader.js +124 -0
  9. package/dist/application/ArtifactInventory/types.js +10 -0
  10. package/dist/application/ArtifactInventory.js +9 -351
  11. package/dist/application/AuthorizedArtifactInventory.js +2 -2
  12. package/dist/application/BinarySession.js +47 -270
  13. package/dist/application/BinarySessionExecution.js +28 -0
  14. package/dist/application/BinarySessionOpen.js +39 -0
  15. package/dist/application/BinarySessionRecords.js +149 -0
  16. package/dist/application/BinarySessionStatus.js +75 -0
  17. package/dist/application/CapabilityInventory.js +24 -2
  18. package/dist/application/ClientRegistrationStatus.js +1 -1
  19. package/dist/application/CrossVersionInventory.js +8 -2
  20. package/dist/application/CrossVersionInvestigation.js +1 -23
  21. package/dist/application/CrossVersionInvestigationCollections.js +23 -0
  22. package/dist/application/DirectAnalysis.js +47 -44
  23. package/dist/application/Doctor.js +156 -238
  24. package/dist/application/DoctorDiagnostics.js +180 -0
  25. package/dist/application/ElectronBoundaryAnalysis.js +23 -7
  26. package/dist/application/ElectronBoundaryGraphWindows.js +32 -8
  27. package/dist/application/EnhancedToolPagination.js +22 -0
  28. package/dist/application/EnhancedToolTypes.js +4 -0
  29. package/dist/application/EnhancedTools.js +20 -37
  30. package/dist/application/InstrumentedJavaScriptReplayHost.js +36 -0
  31. package/dist/application/InvestigationWorkspaceStore.js +24 -10
  32. package/dist/application/JavaScriptArtifactAnalysis.js +45 -6
  33. package/dist/application/JavaScriptArtifactFiles.js +6 -2
  34. package/dist/application/JavaScriptArtifactGraphBuilder.js +23 -1
  35. package/dist/application/JavaScriptArtifactGraphBundlers.js +225 -0
  36. package/dist/application/JavaScriptArtifactGraphContext.js +37 -36
  37. package/dist/application/JavaScriptArtifactGraphDocuments.js +27 -4
  38. package/dist/application/JavaScriptArtifactGraphFindings.js +54 -8
  39. package/dist/application/JavaScriptArtifactGraphStructure.js +29 -101
  40. package/dist/application/JavaScriptArtifactPathResolution.js +269 -0
  41. package/dist/application/JavaScriptArtifactReconstruction.js +1 -1
  42. package/dist/application/JavaScriptJsonModules.js +39 -0
  43. package/dist/application/JavaScriptModuleRelationships.js +262 -0
  44. package/dist/application/JavaScriptReplayPermission.js +16 -0
  45. package/dist/application/JavaScriptReplayService.js +182 -156
  46. package/dist/application/LinuxHopper.js +10 -0
  47. package/dist/application/MacHopper.js +22 -11
  48. package/dist/application/NodeRuntimeCharacterizationService.js +196 -0
  49. package/dist/application/PermissionAuthority.js +106 -63
  50. package/dist/application/ProcessCaptureCapability.js +1 -1
  51. package/dist/application/ProcessCapturePermission.js +13 -0
  52. package/dist/application/ProcessCli.js +2 -12
  53. package/dist/application/ProcessHarness.js +1 -1
  54. package/dist/application/ProjectPermissionStore.js +48 -9
  55. package/dist/application/RealHopperAssertions.js +76 -0
  56. package/dist/application/ReconstructionCoverageService.js +69 -0
  57. package/dist/application/ReconstructionCoverageWorkspaceStore.js +31 -0
  58. package/dist/application/RuntimeExecutableDiagnostics.js +172 -0
  59. package/dist/application/Setup.js +147 -259
  60. package/dist/application/SetupClientConfiguration.js +220 -0
  61. package/dist/application/SetupClients.js +24 -2
  62. package/dist/application/SetupPlan.js +123 -12
  63. package/dist/application/SetupSkill.js +26 -12
  64. package/dist/application/SupportedClients.js +1 -0
  65. package/dist/application/Uninstall.js +6 -3
  66. package/dist/application/WindowsCapabilities.js +86 -0
  67. package/dist/artifacts/AsarArtifactReader.js +11 -0
  68. package/dist/browser/BrowserProcessStartup.js +88 -0
  69. package/dist/browser/CdpCaptureDocuments.js +12 -8
  70. package/dist/browser/CdpCaptureEventBodyShapes.js +75 -0
  71. package/dist/browser/CdpCaptureEventHandlers.js +336 -0
  72. package/dist/browser/CdpCaptureEventHelpers.js +71 -0
  73. package/dist/browser/CdpCaptureEventState.js +1 -0
  74. package/dist/browser/CdpCaptureEventTypes.js +1 -0
  75. package/dist/browser/CdpCaptureEvents.js +29 -496
  76. package/dist/browser/CdpObservationSession.js +51 -8
  77. package/dist/browser/CdpPageCapture.js +12 -133
  78. package/dist/browser/CdpPageCaptureScripts.js +97 -0
  79. package/dist/browser/CdpPageCaptureWorkers.js +46 -0
  80. package/dist/browser/CdpWebMcpDiscovery.js +38 -14
  81. package/dist/browser/PngVisualDiff.js +9 -2
  82. package/dist/catalogIdentity.js +4 -0
  83. package/dist/cli/artifactCommands.js +82 -0
  84. package/dist/cli/coreAnalysisCommands.js +232 -0
  85. package/dist/cli/managedCommands.js +397 -0
  86. package/dist/cli/managedProjectGraphCommand.js +24 -0
  87. package/dist/cli/options.js +9 -0
  88. package/dist/cli/setupCommands.js +108 -0
  89. package/dist/cli/types.js +1 -0
  90. package/dist/cli/utilityCommands.js +122 -0
  91. package/dist/cli.js +10 -768
  92. package/dist/cliApplicationCommands.js +118 -4
  93. package/dist/cliBrowserAdvancedCommands.js +73 -16
  94. package/dist/cliBrowserCommands.js +39 -115
  95. package/dist/cliCommandNames.js +4 -0
  96. package/dist/cliElectronCommands.js +33 -62
  97. package/dist/cliEvidenceCommands.js +13 -2
  98. package/dist/cliInvestigationCommands.js +53 -10
  99. package/dist/cliObservationOptions.js +137 -0
  100. package/dist/cliPolicyCommands.js +80 -46
  101. package/dist/cliSetup.js +289 -0
  102. package/dist/config/environment.js +107 -0
  103. package/dist/config/parseConfig.js +228 -0
  104. package/dist/config/parsers.js +68 -0
  105. package/dist/config/permissions.js +30 -0
  106. package/dist/config/types.js +1 -0
  107. package/dist/config.js +1 -375
  108. package/dist/contracts/applicationToolContracts.js +134 -0
  109. package/dist/contracts/managedToolContracts.js +1 -1
  110. package/dist/contracts/managedWorkflowToolContracts.js +3 -3
  111. package/dist/contracts/providerSelection.js +2 -0
  112. package/dist/contracts/toolContracts.js +2 -2
  113. package/dist/contracts/toolEffects.js +14 -0
  114. package/dist/contracts/toolOutputSchemaGroups.js +232 -0
  115. package/dist/contracts/toolOutputSchemaPrimitives.js +327 -0
  116. package/dist/contracts/toolOutputSchemas.js +2 -550
  117. package/dist/domain/analysisErrorPresentation.js +234 -0
  118. package/dist/domain/analysisErrorProjection.js +284 -0
  119. package/dist/domain/artifactComparison.js +34 -15
  120. package/dist/domain/binaryTarget.js +87 -37
  121. package/dist/domain/browserObservation.js +1 -285
  122. package/dist/domain/browserObservationSchemas.js +286 -0
  123. package/dist/domain/electronStaticAnalysisBrowser.js +14 -2
  124. package/dist/domain/errors.js +4 -486
  125. package/dist/domain/functionComparison.js +4 -165
  126. package/dist/domain/functionComparisonDimensions.js +164 -0
  127. package/dist/domain/javascriptApplicationVersionItems.js +18 -6
  128. package/dist/domain/javascriptApplicationVersionKeys.js +8 -8
  129. package/dist/domain/javascriptExportInstrumentation.js +97 -0
  130. package/dist/domain/javascriptFeatureTraversal.js +19 -7
  131. package/dist/domain/javascriptRuntimeStaticCandidates.js +3 -1
  132. package/dist/domain/javascriptSemanticAnalysis.js +386 -0
  133. package/dist/domain/javascriptSemanticIr.js +30 -0
  134. package/dist/domain/javascriptSemanticProjection.js +366 -0
  135. package/dist/domain/javascriptSemanticState.js +40 -0
  136. package/dist/domain/javascriptSemanticTraversal.js +22 -0
  137. package/dist/domain/javascriptSemanticValues.js +352 -0
  138. package/dist/domain/javascriptStaticAnalysis.js +5 -221
  139. package/dist/domain/javascriptStaticAnalysisBundler.js +209 -0
  140. package/dist/domain/javascriptStaticAnalysisCalls.js +180 -0
  141. package/dist/domain/javascriptStaticAnalysisHelpers.js +71 -2
  142. package/dist/domain/knownPageCoverage.js +21 -0
  143. package/dist/domain/managedApplicationGraph.js +14 -364
  144. package/dist/domain/managedApplicationGraphCoverage.js +170 -0
  145. package/dist/domain/managedApplicationGraphNodeCoverage.js +6 -0
  146. package/dist/domain/managedApplicationGraphNodes.js +336 -0
  147. package/dist/domain/managedArtifact.js +16 -8
  148. package/dist/domain/managedMemberComparison.js +37 -391
  149. package/dist/domain/managedMemberComparisonCoverage.js +72 -0
  150. package/dist/domain/managedMemberComparisonMatch.js +356 -0
  151. package/dist/domain/managedNativeVerification.js +27 -389
  152. package/dist/domain/managedNativeVerificationMatch.js +272 -0
  153. package/dist/domain/managedNativeVerificationSchemas.js +141 -0
  154. package/dist/domain/managedReconstruction.js +82 -69
  155. package/dist/domain/managedRuntimeCorrelation.js +12 -3
  156. package/dist/domain/nodeRuntimeCharacterization.js +66 -0
  157. package/dist/domain/permissionPolicy.js +2 -0
  158. package/dist/domain/reconstructionCoverage.js +387 -0
  159. package/dist/domain/reconstructionCoverageEvaluation.js +212 -0
  160. package/dist/domain/reconstructionVerificationSchemas.js +8 -0
  161. package/dist/domain/runtimeCharacterization.js +85 -0
  162. package/dist/domain/webBundleAnalyzer.js +3 -409
  163. package/dist/domain/webBundleAnalyzerAst.js +77 -0
  164. package/dist/domain/webBundleAnalyzerInspection.js +307 -0
  165. package/dist/domain/webBundleAnalyzerResult.js +120 -0
  166. package/dist/dotnet/ManagedArtifactInspector.js +50 -29
  167. package/dist/dotnet/ManagedMemberInspector.js +51 -785
  168. package/dist/dotnet/ManagedMemberInspectorCore.js +297 -0
  169. package/dist/dotnet/ManagedMemberInstructionDecoder.js +427 -0
  170. package/dist/dotnet/ManagedMemberRows.js +120 -0
  171. package/dist/dotnet/ManagedMetadataInventory.js +42 -306
  172. package/dist/dotnet/ManagedMetadataInventoryRows.js +292 -0
  173. package/dist/dotnet/ManagedMetadataLayout.js +48 -111
  174. package/dist/dotnet/ManagedMetadataRowSizes.js +54 -0
  175. package/dist/dotnet/ManagedMethodBodyReader.js +143 -0
  176. package/dist/dotnet/ManagedNativeBoundaryHelpers.js +311 -0
  177. package/dist/dotnet/ManagedNativeBoundaryInspector.js +102 -350
  178. package/dist/dotnet/ManagedPeReader.js +59 -28
  179. package/dist/generatedPackageMetadata.js +2 -2
  180. package/dist/ghidra/GhidraClient.js +76 -91
  181. package/dist/ghidra/GhidraClientStartup.js +29 -0
  182. package/dist/ghidra/GhidraClientWire.js +34 -0
  183. package/dist/ghidra/GhidraDefaults.js +1 -1
  184. package/dist/ghidra/GhidraDiagnostics.js +6 -1
  185. package/dist/ghidra/GhidraInstallation.js +20 -15
  186. package/dist/ghidra/GhidraLauncher.js +93 -29
  187. package/dist/ghidra/GhidraProvider.js +61 -161
  188. package/dist/ghidra/GhidraProviderCapabilities.js +173 -0
  189. package/dist/ghidra/GhidraSessionValues.js +2 -0
  190. package/dist/ghidra/GhidraSocketConnection.js +44 -7
  191. package/dist/ghidra/GhidraTargetSnapshot.js +30 -0
  192. package/dist/ghidra/GhidraTransport.js +71 -0
  193. package/dist/identity.js +3 -2
  194. package/dist/main/messages.js +5 -0
  195. package/dist/main/reload.js +49 -0
  196. package/dist/main/shutdown.js +25 -0
  197. package/dist/main/startup.js +14 -0
  198. package/dist/main/state.js +26 -0
  199. package/dist/main/transport.js +61 -0
  200. package/dist/main/types.js +1 -0
  201. package/dist/main.js +35 -146
  202. package/dist/process/PrivateRuntimeRoot.js +3 -2
  203. package/dist/process/ProcessOwnership.js +35 -0
  204. package/dist/process/ProviderProcess.js +2 -0
  205. package/dist/replay/JavaScriptReplayWorker.js +64 -50
  206. package/dist/replay/LinuxJavaScriptReplayRunner.js +173 -390
  207. package/dist/replay/LinuxJavaScriptReplaySandbox.js +140 -0
  208. package/dist/replay/ReplayOutcome.js +95 -0
  209. package/dist/replay/ReplayProcessLifecycle.js +71 -0
  210. package/dist/server/ProcessCaptureElicitation.js +157 -0
  211. package/dist/server/createServer.js +119 -54
  212. package/dist/server/registerApplicationTools/characterization.js +57 -0
  213. package/dist/server/registerApplicationTools/compareVersions.js +34 -0
  214. package/dist/server/registerApplicationTools/controlledReplay.js +38 -0
  215. package/dist/server/registerApplicationTools/coverage.js +84 -0
  216. package/dist/server/registerApplicationTools/helpers.js +42 -0
  217. package/dist/server/registerApplicationTools/traceFeature.js +26 -0
  218. package/dist/server/registerApplicationTools/types.js +1 -0
  219. package/dist/server/registerApplicationTools.js +10 -112
  220. package/dist/server/registerBrowserTools.js +59 -83
  221. package/dist/server/registerElectronTools.js +31 -46
  222. package/dist/server/registerEnhancedTools.js +34 -42
  223. package/dist/server/registerEvidenceResources.js +54 -0
  224. package/dist/server/registerInvestigationTools/helpers.js +74 -0
  225. package/dist/server/registerInvestigationTools/types.js +1 -0
  226. package/dist/server/registerInvestigationTools.js +83 -116
  227. package/dist/server/registerManagedWorkflowTools/compareManagedMembers.js +65 -0
  228. package/dist/server/registerManagedWorkflowTools/contract.js +8 -0
  229. package/dist/server/registerManagedWorkflowTools/evidence.js +43 -0
  230. package/dist/server/registerManagedWorkflowTools/importManagedReconstruction.js +39 -0
  231. package/dist/server/registerManagedWorkflowTools/planManagedRuntimeCorrelation.js +39 -0
  232. package/dist/server/registerManagedWorkflowTools/projectManagedApplicationGraph.js +52 -0
  233. package/dist/server/registerManagedWorkflowTools/types.js +1 -0
  234. package/dist/server/registerManagedWorkflowTools/verifyManagedNativeBoundaries.js +75 -0
  235. package/dist/server/registerManagedWorkflowTools.js +10 -274
  236. package/dist/server/registerOfficialTools.js +73 -59
  237. package/dist/server/registerSessionRecordTools.js +137 -0
  238. package/dist/server/registerSessionStatusTool.js +2 -1
  239. package/dist/server/registerSessionTools.js +60 -164
  240. package/dist/server/sessionAvailabilityPolicy.js +10 -0
  241. package/dist/server/toolRegistrationOptions.js +101 -3
  242. package/dist/server/toolResult.js +5 -0
  243. package/package.json +10 -11
  244. package/skills/{rea-analysis → reverse-engineer-anything}/SKILL.md +20 -7
  245. package/scripts/prepare-node-pty.mjs +0 -35
  246. package/scripts/rebuild-native.mjs +0 -22
@@ -4,7 +4,7 @@ import { withinRoot } from "../reference/ReferenceSourceReaderPaths.js";
4
4
  import { scanCanonicalArtifactInventory, } from "./ArtifactInventory.js";
5
5
  import { canonicalizeConfiguredRoots } from "./ConfiguredRoots.js";
6
6
  /** Resolve, authorize, and scan one artifact without a second path resolution. */
7
- export const scanAuthorizedArtifactInventory = async (inputPath, roots, limits, signal, integrity) => {
7
+ export const scanAuthorizedArtifactInventory = async (inputPath, roots, limits, options = {}) => {
8
8
  if (roots.length === 0)
9
9
  throw new ArtifactReaderFailure("path", "Artifact input roots are disabled");
10
10
  const [path, approved] = await Promise.all([
@@ -13,5 +13,5 @@ export const scanAuthorizedArtifactInventory = async (inputPath, roots, limits,
13
13
  ]);
14
14
  if (!approved.some((root) => withinRoot(root, path)))
15
15
  throw new ArtifactReaderFailure("path", "Artifact path is outside approved input roots");
16
- return scanCanonicalArtifactInventory(path, limits, signal, undefined, integrity);
16
+ return scanCanonicalArtifactInventory(path, limits, options);
17
17
  };
@@ -1,18 +1,13 @@
1
- import { parseBinaryTarget } from "../domain/binaryTarget.js";
2
1
  import { analysisProfilesEqual, } from "../domain/analysisProfile.js";
3
- import { AnalysisCapabilityUnavailableError, EvidenceIntegrityError, EvidenceLimitError, AnalysisCancelledError, NoBinaryOpenError, ProviderAdapterError, } from "../domain/errors.js";
2
+ import { AnalysisCancelledError, ProviderAdapterError, } from "../domain/errors.js";
4
3
  import { err, ok } from "../domain/result.js";
5
- import { createEvidence } from "../domain/evidence.js";
6
- import { evidenceBundleForTarget } from "../domain/evidenceBundle.js";
7
- import { EvidenceLedger } from "./EvidenceLedger.js";
8
- import { UnknownRegistryError } from "../domain/errors.js";
9
4
  import { enhancedToolNameSchema } from "../contracts/enhancedInputs.js";
10
5
  import { OFFICIAL_TOOL_CONTRACTS } from "../contracts/toolContracts.js";
11
- import { snapshotMatchesBinding, snapshotMatchesTarget, snapshotTarget, } from "../domain/analysisSnapshot.js";
12
- import { AnalysisSnapshotCache, isSnapshotCacheable, } from "./AnalysisSnapshotCache.js";
13
- import { UNKNOWN_REGISTRY_PROVIDER, unknownEvidenceLinks, unknownMutationEvidence, } from "./UnknownEvidence.js";
14
- import { parseInvestigationWorkspace, } from "../domain/investigationWorkspace.js";
15
6
  import { SessionProviderRouter, } from "./SessionProviderRouter.js";
7
+ import { BinarySessionRecords } from "./BinarySessionRecords.js";
8
+ import { binarySessionStatus } from "./BinarySessionStatus.js";
9
+ import { resolveSessionOpen, } from "./BinarySessionOpen.js";
10
+ import { prepareSessionExecution } from "./BinarySessionExecution.js";
16
11
  const OFFICIAL_OPERATIONS = new Set(OFFICIAL_TOOL_CONTRACTS.map(({ name }) => name));
17
12
  /**
18
13
  * Owns the single active target shared by CLI and MCP adapters.
@@ -21,22 +16,16 @@ const OFFICIAL_OPERATIONS = new Set(OFFICIAL_TOOL_CONTRACTS.map(({ name }) => na
21
16
  * active provider client. A failed switch recreates the previous target instead
22
17
  * of retaining a client whose resources were already shut down.
23
18
  */
24
- export class BinarySession {
19
+ export class BinarySession extends BinarySessionRecords {
25
20
  provider;
26
21
  #active;
27
22
  #transition = Promise.resolve();
28
23
  #calls = new Set();
29
24
  #providerRouter;
30
- #evidence = new EvidenceLedger({
31
- maxRecords: 10_000,
32
- maxBytes: 64 * 1024 * 1024,
33
- });
34
- #snapshot = new AnalysisSnapshotCache();
35
- #investigationWorkspaces = new Map();
36
25
  #runtimeAvailability = new Map();
37
26
  #availabilityListeners = new Set();
38
- #snapshotInvalidated = false;
39
27
  constructor(provider, options = {}) {
28
+ super();
40
29
  this.provider = provider;
41
30
  this.#providerRouter =
42
31
  provider instanceof SessionProviderRouter
@@ -90,104 +79,6 @@ export class BinarySession {
90
79
  this.#availabilityListeners.add(listener);
91
80
  return () => this.#availabilityListeners.delete(listener);
92
81
  }
93
- /** Add one successful public observation to the session ledger. */
94
- recordEvidence(evidence) {
95
- return this.#evidence.record(evidence);
96
- }
97
- /** Check that comparison input Evidence is already session-owned. */
98
- hasEvidence(evidenceId) {
99
- return this.#evidence.has(evidenceId);
100
- }
101
- /** Read one detached session-owned Evidence record by semantic ID. */
102
- evidenceById(evidenceId) {
103
- return this.#evidence.get(evidenceId);
104
- }
105
- /** Return a deterministic snapshot without clearing session evidence. */
106
- exportEvidenceBundle() {
107
- return this.#evidence.export();
108
- }
109
- /** Atomically merge a validated evidence bundle into this session. */
110
- importEvidenceBundle(bundle) {
111
- return this.#evidence.import(bundle);
112
- }
113
- /** Export immutable cached calls and session evidence for the active target. */
114
- exportAnalysisSnapshot() {
115
- if (this.#snapshotInvalidated)
116
- return err(new EvidenceIntegrityError("Analysis snapshots are unavailable after analysis metadata mutations"));
117
- const target = this.#active?.target;
118
- const profile = this.#active?.profile ?? undefined;
119
- if (target !== undefined && profile === undefined)
120
- return err(new EvidenceIntegrityError("Analysis snapshots require a concrete provider analysis profile"));
121
- return this.#snapshot.export(target, profile, target === undefined
122
- ? this.#evidence.export()
123
- : evidenceBundleForTarget(this.#evidence.export(), target.sha256));
124
- }
125
- /** Stage validated cached calls for an identical binary and merge its evidence. */
126
- importAnalysisSnapshot(snapshot) {
127
- const active = this.#active;
128
- if (active?.profile === null)
129
- return err(new EvidenceIntegrityError("Analysis snapshot profile_mismatch: the active target has no concrete analysis profile"));
130
- return this.#snapshot.import(snapshot, active === undefined
131
- ? undefined
132
- : { target: active.target, profile: active.profile }, (bundle) => this.#evidence.import(bundle));
133
- }
134
- /** Retain one validated immutable workspace revision for session resources. */
135
- retainInvestigationWorkspace(workspace) {
136
- const parsed = parseInvestigationWorkspace(workspace);
137
- const key = `${parsed.workspace_id}:${String(parsed.revision)}`;
138
- if (this.#investigationWorkspaces.has(key))
139
- return "duplicate";
140
- this.#investigationWorkspaces.set(key, parsed);
141
- return "added";
142
- }
143
- /** Read one session-retained immutable workspace revision. */
144
- investigationWorkspace(workspaceId, revision) {
145
- const workspace = this.#investigationWorkspaces.get(`${workspaceId}:${String(revision)}`);
146
- return workspace === undefined ? undefined : structuredClone(workspace);
147
- }
148
- /** List retained workspace revisions in canonical identity order. */
149
- investigationWorkspaces() {
150
- return [...this.#investigationWorkspaces.values()]
151
- .sort((left, right) => left.workspace_id.localeCompare(right.workspace_id) ||
152
- left.revision - right.revision)
153
- .map((workspace) => structuredClone(workspace));
154
- }
155
- /** Create an approved residual unknown and immutable mutation evidence. */
156
- recordUnknown(input) {
157
- const evidence = unknownMutationEvidence(this.#active?.target, input);
158
- return this.#evidence.recordUnknown(input, evidence);
159
- }
160
- /** Atomically record derived Evidence and its approved residual unknown. */
161
- recordEvidenceWithUnknown(evidence, input) {
162
- return this.#evidence.recordWithUnknown(evidence, input, unknownMutationEvidence(undefined, input));
163
- }
164
- /** Update one unknown using compare-and-swap revision semantics. */
165
- updateUnknown(input) {
166
- const evidence = createEvidence(this.#active?.target, UNKNOWN_REGISTRY_PROVIDER, {
167
- predicateType: "rea.residual-unknown-mutation/v1",
168
- operation: "update_unknown",
169
- parameters: {
170
- unknown_id: input.unknown_id,
171
- expected_revision: input.expected_revision,
172
- },
173
- result: { action: "update", status: input.status },
174
- confidence: "derived",
175
- authority: "analyst-inference",
176
- evidenceLinks: unknownEvidenceLinks(input),
177
- limitations: [
178
- "Registry mutation evidence records analyst intent, not proof of the answer.",
179
- ],
180
- });
181
- return this.#evidence.updateUnknown(input, evidence);
182
- }
183
- /** Query stable current residual-unknown heads. */
184
- listUnknowns(filters = {}) {
185
- return this.#evidence.listUnknowns(filters);
186
- }
187
- /** Check whether current head is a bundle-valid resolved state. */
188
- verifyUnknownResolution(unknownId) {
189
- return this.#evidence.verifyUnknownResolution(unknownId);
190
- }
191
82
  /**
192
83
  * Open or switch targets after draining calls against the current target.
193
84
  * Returns the switch failure even if best-effort restoration also fails.
@@ -196,27 +87,17 @@ export class BinarySession {
196
87
  return this.#serialize(async () => {
197
88
  if (isAborted(options.signal))
198
89
  return err(new AnalysisCancelledError("open_binary"));
199
- const parsed = await parseBinaryTarget(path, process.cwd(), process.arch, options.targetKind);
200
- if (!parsed.ok)
201
- return parsed;
202
- const sameTarget = this.#active?.target.path === parsed.value.path &&
203
- snapshotMatchesTarget(snapshotTarget(this.#active.target), parsed.value);
204
- const resolvedRoute = sameTarget &&
205
- options.providerId === undefined &&
206
- this.#active !== undefined
207
- ? ok(this.#active.route)
208
- : await this.#providerRouter.resolve(parsed.value, options.providerId, options.signal);
209
- if (!resolvedRoute.ok)
210
- return resolvedRoute;
211
- const route = resolvedRoute.value;
90
+ const resolved = await resolveSessionOpen({
91
+ router: this.#providerRouter,
92
+ current: this.#active,
93
+ path,
94
+ options,
95
+ stagedSnapshotMatches: (target, profile) => this.matchesSnapshot(target, profile),
96
+ });
97
+ if (!resolved.ok)
98
+ return resolved;
99
+ const { target, route, sameTarget } = resolved.value;
212
100
  const { profile, compatibility } = route;
213
- if (options.snapshot !== undefined &&
214
- (profile === null ||
215
- !snapshotMatchesBinding(options.snapshot, parsed.value, profile)))
216
- return err(new EvidenceIntegrityError("Analysis snapshot profile_mismatch: target, provider, or analysis profile does not match the requested binary"));
217
- if (this.#active === undefined &&
218
- !this.#snapshot.matches(parsed.value, profile ?? undefined))
219
- return err(new EvidenceIntegrityError("Analysis snapshot profile_mismatch: staged target, provider, or analysis profile does not match"));
220
101
  if (isAborted(options.signal))
221
102
  return err(new AnalysisCancelledError("open_binary"));
222
103
  const activeProfile = this.#active?.profile;
@@ -229,13 +110,13 @@ export class BinarySession {
229
110
  if (!imported.ok)
230
111
  return imported;
231
112
  }
232
- return ok(parsed.value);
113
+ return ok(target);
233
114
  }
234
115
  await this.#drainCalls();
235
116
  const previous = this.#active;
236
117
  this.#active = undefined;
237
118
  await previous?.client.close();
238
- const client = route.createClient(parsed.value);
119
+ const client = route.createClient(target);
239
120
  const started = await client.execute("health", {}, options);
240
121
  if (!started.ok) {
241
122
  await client.close();
@@ -248,7 +129,7 @@ export class BinarySession {
248
129
  return err(new AnalysisCancelledError("open_binary"));
249
130
  }
250
131
  this.#active = {
251
- target: parsed.value,
132
+ target,
252
133
  client,
253
134
  profile,
254
135
  compatibility: structuredClone(compatibility),
@@ -256,9 +137,9 @@ export class BinarySession {
256
137
  };
257
138
  this.#clearRuntimeAvailability();
258
139
  if (profile === null)
259
- this.#snapshot.clear();
140
+ this.clearSnapshot();
260
141
  else
261
- this.#snapshot.select(parsed.value, profile);
142
+ this.selectSnapshot(target, profile);
262
143
  if (options.snapshot !== undefined) {
263
144
  const imported = this.importAnalysisSnapshot(options.snapshot);
264
145
  if (!imported.ok) {
@@ -268,8 +149,8 @@ export class BinarySession {
268
149
  return imported;
269
150
  }
270
151
  }
271
- this.#snapshotInvalidated = false;
272
- return ok(parsed.value);
152
+ this.resetSnapshotInvalidation();
153
+ return ok(target);
273
154
  });
274
155
  }
275
156
  /** Close the active target, if any. */
@@ -279,90 +160,19 @@ export class BinarySession {
279
160
  this.#active = undefined;
280
161
  await this.#drainCalls();
281
162
  await previous?.client.close();
282
- this.#evidence.clear();
283
- this.#snapshot.clear();
284
- this.#snapshotInvalidated = false;
163
+ this.clearSessionRecords();
285
164
  this.#clearRuntimeAvailability();
286
165
  return ok(null);
287
166
  });
288
167
  }
289
168
  /** Describe the active binary session. */
290
169
  status() {
291
- const target = this.#active?.target;
292
- const route = this.#currentRoute();
293
- const configuredProvider = this.#providerRouter.configuredIdentity();
294
- const provider = {
295
- id: configuredProvider.id,
296
- name: configuredProvider.name,
297
- version: configuredProvider.version,
298
- };
299
- const providerList = this.#providerRouter
300
- .providerIdentities(route)
301
- .map(({ id, name, version }) => ({ id, name, version }));
302
- const capabilities = route.capabilities === undefined
303
- ? []
304
- : [...route.capabilities.values()]
305
- .sort((left, right) => left.operation.localeCompare(right.operation))
306
- .map((descriptor) => ({
307
- ...descriptor,
308
- ...this.#runtimeAvailability.get(descriptor.operation),
309
- }))
310
- .map((descriptor) => capabilityStatus(descriptor));
311
- const analysisProviderBinding = route.binding === null
312
- ? null
313
- : {
314
- provider: {
315
- id: route.binding.identity.id,
316
- name: route.binding.identity.name,
317
- version: route.binding.identity.version,
318
- },
319
- selection_source: route.binding.selectionSource,
320
- analysis_profile: structuredClone(route.binding.profile),
321
- };
322
- const analysisProviderCandidates = this.#providerRouter
323
- .candidateStatuses(route)
324
- .map((candidate) => ({
325
- provider: {
326
- id: candidate.provider.id,
327
- name: candidate.provider.name,
328
- version: candidate.provider.version,
329
- },
330
- availability: {
331
- status: candidate.availability.status,
332
- code: candidate.availability.code,
333
- reason: candidate.availability.reason,
334
- diagnostics: structuredClone(candidate.availability.diagnostics),
335
- },
336
- target_support: {
337
- status: candidate.targetSupport.status,
338
- code: candidate.targetSupport.code,
339
- reason: candidate.targetSupport.reason,
340
- diagnostics: structuredClone(candidate.targetSupport.diagnostics),
341
- },
342
- selected: candidate.selected,
343
- capabilities: candidate.capabilities
344
- .slice()
345
- .sort((left, right) => left.operation.localeCompare(right.operation))
346
- .map((descriptor) => capabilityStatus(descriptor)),
347
- }));
348
- const common = {
349
- provider,
350
- providers: providerList,
351
- capabilities,
352
- analysis_provider_binding: analysisProviderBinding,
353
- analysis_provider_candidates: analysisProviderCandidates,
354
- };
355
- return target === undefined
356
- ? { open: false, ...common }
357
- : {
358
- open: true,
359
- ...common,
360
- path: target.path,
361
- sha256: target.sha256,
362
- format: target.format,
363
- kind: target.kind,
364
- architecture: target.architecture ?? null,
365
- };
170
+ return binarySessionStatus({
171
+ target: this.#active?.target,
172
+ route: this.#currentRoute(),
173
+ router: this.#providerRouter,
174
+ runtimeAvailability: this.#runtimeAvailability,
175
+ });
366
176
  }
367
177
  /** Return the immutable artifact identity captured before its provider started. */
368
178
  activeTarget() {
@@ -370,6 +180,9 @@ export class BinarySession {
370
180
  ? undefined
371
181
  : structuredClone(this.#active.target);
372
182
  }
183
+ activeAnalysisBinding() {
184
+ return this.#active;
185
+ }
373
186
  /**
374
187
  * Invoke a provider operation against the active target.
375
188
  * Calls may overlap, but a pending target transition prevents new calls from
@@ -379,27 +192,16 @@ export class BinarySession {
379
192
  const transitioned = await this.#waitForTransition(name, options?.signal);
380
193
  if (!transitioned.ok)
381
194
  return transitioned;
382
- const active = this.#active;
383
- if (active === undefined)
384
- return err(new NoBinaryOpenError());
385
- const capability = active.route.capabilities?.get(name);
386
- if (active.route.capabilities !== undefined &&
387
- capability?.available !== true) {
388
- const selectionError = this.#providerRouter.unboundOperationError(name, active.route);
389
- if (selectionError !== undefined)
390
- return err(selectionError);
391
- return err(new AnalysisCapabilityUnavailableError(active.route.binding?.identity.id ?? active.route.identity.id, name, capability?.reason ?? "operation is not declared by this provider"));
392
- }
393
- const profile = active.profile !== null &&
394
- (capability === undefined ||
395
- capability.provider.id === active.profile.provider.id)
396
- ? active.profile
397
- : undefined;
398
- const cacheable = profile !== undefined &&
399
- isSnapshotCacheable(name, capability, arguments_);
400
- const cached = cacheable
401
- ? this.#snapshot.lookup(active.target, profile, name, arguments_)
402
- : undefined;
195
+ const prepared = prepareSessionExecution({
196
+ active: this.#active,
197
+ operation: name,
198
+ parameters: arguments_,
199
+ unboundOperationError: (operation, route) => this.#providerRouter.unboundOperationError(operation, route),
200
+ lookupSnapshot: (target, profile, operation, parameters) => this.lookupSnapshot(target, profile, operation, parameters),
201
+ });
202
+ if (!prepared.ok)
203
+ return prepared;
204
+ const { active, capability, profile, cacheable, cached } = prepared.value;
403
205
  if (cached !== undefined)
404
206
  return ok(cached);
405
207
  const call = active.client.execute(name, arguments_, options);
@@ -408,8 +210,8 @@ export class BinarySession {
408
210
  const result = await call;
409
211
  const profiled = commitExecutionProfile(name, result, profile);
410
212
  this.#observeRuntimeAvailability(name, profiled);
411
- if (profiled.ok && cacheable)
412
- this.#snapshot.record({
213
+ if (profiled.ok && cacheable && profile !== undefined)
214
+ this.recordSnapshot({
413
215
  target: active.target,
414
216
  profile,
415
217
  operation: name,
@@ -417,8 +219,7 @@ export class BinarySession {
417
219
  execution: profiled.value,
418
220
  });
419
221
  else if (profiled.ok && capability?.effects.mutatesArtifact === true) {
420
- this.#snapshot.clear();
421
- this.#snapshotInvalidated = true;
222
+ this.invalidateSnapshot();
422
223
  }
423
224
  return profiled;
424
225
  }
@@ -552,27 +353,3 @@ const commitExecutionProfile = (operation, result, profile) => {
552
353
  analysisProfile: structuredClone(profile),
553
354
  });
554
355
  };
555
- const capabilityStatus = (descriptor) => ({
556
- operation: descriptor.operation,
557
- available: descriptor.available,
558
- reason: descriptor.reason,
559
- input_contract_version: descriptor.inputContractVersion,
560
- output_contract_version: descriptor.outputContractVersion,
561
- pagination: descriptor.pagination,
562
- exhaustive: descriptor.exhaustive,
563
- effects: {
564
- mutates_artifact: descriptor.effects.mutatesArtifact,
565
- launches_process: descriptor.effects.launchesProcess,
566
- may_show_ui: descriptor.effects.mayShowUi,
567
- may_access_network: descriptor.effects.mayAccessNetwork,
568
- may_write_filesystem: descriptor.effects.mayWriteFilesystem,
569
- changes_permissions: descriptor.effects.changesPermissions,
570
- requires_root: descriptor.effects.requiresRoot,
571
- },
572
- limits: {
573
- max_results: descriptor.limits.maxResults,
574
- max_payload_bytes: descriptor.limits.maxPayloadBytes,
575
- timeout_ms: descriptor.limits.timeoutMs,
576
- },
577
- limitations: [...descriptor.limitations],
578
- });
@@ -0,0 +1,28 @@
1
+ import { AnalysisCapabilityUnavailableError, NoBinaryOpenError, } from "../domain/errors.js";
2
+ import { err, ok } from "../domain/result.js";
3
+ import { isSnapshotCacheable } from "./AnalysisSnapshotCache.js";
4
+ /** Resolve capability, profile, and snapshot state before invoking a provider. */
5
+ export const prepareSessionExecution = (input) => {
6
+ const { active, operation, parameters, unboundOperationError, lookupSnapshot, } = input;
7
+ if (active === undefined)
8
+ return err(new NoBinaryOpenError());
9
+ const capability = active.route.capabilities?.get(operation);
10
+ if (active.route.capabilities !== undefined &&
11
+ capability?.available !== true) {
12
+ const selectionError = unboundOperationError(operation, active.route);
13
+ if (selectionError !== undefined)
14
+ return err(selectionError);
15
+ return err(new AnalysisCapabilityUnavailableError(active.route.binding?.identity.id ?? active.route.identity.id, operation, capability?.reason ?? "operation is not declared by this provider"));
16
+ }
17
+ const profile = active.profile !== null &&
18
+ (capability === undefined ||
19
+ capability.provider.id === active.profile.provider.id)
20
+ ? active.profile
21
+ : undefined;
22
+ const cacheable = profile !== undefined &&
23
+ isSnapshotCacheable(operation, capability, parameters);
24
+ const cached = cacheable
25
+ ? lookupSnapshot(active.target, profile, operation, parameters)
26
+ : undefined;
27
+ return ok({ active, capability, profile, cacheable, cached });
28
+ };
@@ -0,0 +1,39 @@
1
+ import { snapshotMatchesBinding, snapshotMatchesTarget, snapshotTarget, } from "../domain/analysisSnapshot.js";
2
+ import { parseBinaryTarget, } from "../domain/binaryTarget.js";
3
+ import { EvidenceIntegrityError, } from "../domain/errors.js";
4
+ import { err } from "../domain/result.js";
5
+ import { SessionProviderRouter } from "./SessionProviderRouter.js";
6
+ /** Parse a target, resolve its provider route, and validate snapshot binding. */
7
+ export const resolveSessionOpen = async (input) => {
8
+ const { router, current, path, options, stagedSnapshotMatches } = input;
9
+ const parsed = await parseBinaryTarget(path, process.cwd(), process.arch, options.targetKind);
10
+ if (!parsed.ok)
11
+ return parsed;
12
+ const target = parsed.value;
13
+ const sameTarget = current?.target.path === target.path &&
14
+ snapshotMatchesTarget(snapshotTarget(current.target), target);
15
+ const resolvedRoute = sameTarget && options.providerId === undefined && current !== undefined
16
+ ? { ok: true, value: current.route }
17
+ : await router.resolve(target, options.providerId, options.signal);
18
+ if (!resolvedRoute.ok)
19
+ return resolvedRoute;
20
+ const route = resolvedRoute.value;
21
+ const snapshotError = validateSnapshot({
22
+ snapshot: options.snapshot,
23
+ target,
24
+ profile: route.profile,
25
+ current,
26
+ stagedSnapshotMatches,
27
+ });
28
+ if (snapshotError !== undefined)
29
+ return err(snapshotError);
30
+ return { ok: true, value: { target, route, sameTarget } };
31
+ };
32
+ const validateSnapshot = ({ snapshot, target, profile, current, stagedSnapshotMatches, }) => {
33
+ if (snapshot !== undefined &&
34
+ (profile === null || !snapshotMatchesBinding(snapshot, target, profile)))
35
+ return new EvidenceIntegrityError("Analysis snapshot profile_mismatch: target, provider, or analysis profile does not match the requested binary");
36
+ if (current === undefined && !stagedSnapshotMatches(target, profile))
37
+ return new EvidenceIntegrityError("Analysis snapshot profile_mismatch: staged target, provider, or analysis profile does not match");
38
+ return undefined;
39
+ };
@@ -0,0 +1,149 @@
1
+ import { createEvidence } from "../domain/evidence.js";
2
+ import { evidenceBundleForTarget } from "../domain/evidenceBundle.js";
3
+ import { EvidenceIntegrityError, } from "../domain/errors.js";
4
+ import { parseInvestigationWorkspace, } from "../domain/investigationWorkspace.js";
5
+ import { parseReconstructionCoverageWorkspace, } from "../domain/reconstructionCoverage.js";
6
+ import { err } from "../domain/result.js";
7
+ import { AnalysisSnapshotCache } from "./AnalysisSnapshotCache.js";
8
+ import { EvidenceLedger } from "./EvidenceLedger.js";
9
+ import { UNKNOWN_REGISTRY_PROVIDER, unknownEvidenceLinks, unknownMutationEvidence, } from "./UnknownEvidence.js";
10
+ /** Owns session evidence, snapshots, workspaces, and residual unknowns. */
11
+ export class BinarySessionRecords {
12
+ #evidence = new EvidenceLedger({
13
+ maxRecords: 10_000,
14
+ maxBytes: 64 * 1024 * 1024,
15
+ });
16
+ #snapshot = new AnalysisSnapshotCache();
17
+ #investigationWorkspaces = new Map();
18
+ #coverageWorkspaces = new Map();
19
+ #snapshotInvalidated = false;
20
+ recordEvidence(evidence) {
21
+ return this.#evidence.record(evidence);
22
+ }
23
+ hasEvidence(evidenceId) {
24
+ return this.#evidence.has(evidenceId);
25
+ }
26
+ evidenceById(evidenceId) {
27
+ return this.#evidence.get(evidenceId);
28
+ }
29
+ exportEvidenceBundle() {
30
+ return this.#evidence.export();
31
+ }
32
+ importEvidenceBundle(bundle) {
33
+ return this.#evidence.import(bundle);
34
+ }
35
+ exportAnalysisSnapshot() {
36
+ const active = this.activeAnalysisBinding();
37
+ if (this.#snapshotInvalidated)
38
+ return err(new EvidenceIntegrityError("Analysis snapshots are unavailable after analysis metadata mutations"));
39
+ const target = active?.target;
40
+ const profile = active?.profile ?? undefined;
41
+ if (target !== undefined && profile === undefined)
42
+ return err(new EvidenceIntegrityError("Analysis snapshots require a concrete provider analysis profile"));
43
+ return this.#snapshot.export(target, profile, target === undefined
44
+ ? this.#evidence.export()
45
+ : evidenceBundleForTarget(this.#evidence.export(), target.sha256));
46
+ }
47
+ importAnalysisSnapshot(snapshot) {
48
+ const active = this.activeAnalysisBinding();
49
+ if (active?.profile === null)
50
+ return err(new EvidenceIntegrityError("Analysis snapshot profile_mismatch: the active target has no concrete analysis profile"));
51
+ return this.#snapshot.import(snapshot, active === undefined
52
+ ? undefined
53
+ : { target: active.target, profile: active.profile }, (bundle) => this.#evidence.import(bundle));
54
+ }
55
+ matchesSnapshot(target, profile) {
56
+ return this.#snapshot.matches(target, profile ?? undefined);
57
+ }
58
+ selectSnapshot(target, profile) {
59
+ this.#snapshot.select(target, profile);
60
+ }
61
+ lookupSnapshot(target, profile, operation, parameters) {
62
+ return this.#snapshot.lookup(target, profile, operation, parameters);
63
+ }
64
+ recordSnapshot(input) {
65
+ this.#snapshot.record(input);
66
+ }
67
+ invalidateSnapshot() {
68
+ this.#snapshot.clear();
69
+ this.#snapshotInvalidated = true;
70
+ }
71
+ resetSnapshotInvalidation() {
72
+ this.#snapshotInvalidated = false;
73
+ }
74
+ clearSnapshot() {
75
+ this.#snapshot.clear();
76
+ }
77
+ clearSessionRecords() {
78
+ this.#evidence.clear();
79
+ this.#snapshot.clear();
80
+ this.#snapshotInvalidated = false;
81
+ }
82
+ retainInvestigationWorkspace(workspace) {
83
+ const parsed = parseInvestigationWorkspace(workspace);
84
+ const key = workspaceKey(parsed.workspace_id, parsed.revision);
85
+ if (this.#investigationWorkspaces.has(key))
86
+ return "duplicate";
87
+ this.#investigationWorkspaces.set(key, parsed);
88
+ return "added";
89
+ }
90
+ investigationWorkspace(workspaceId, revision) {
91
+ const workspace = this.#investigationWorkspaces.get(workspaceKey(workspaceId, revision));
92
+ return workspace === undefined ? undefined : structuredClone(workspace);
93
+ }
94
+ investigationWorkspaces() {
95
+ return sortedWorkspaces(this.#investigationWorkspaces.values());
96
+ }
97
+ retainReconstructionCoverageWorkspace(workspace) {
98
+ const parsed = parseReconstructionCoverageWorkspace(workspace);
99
+ const key = workspaceKey(parsed.workspace_id, parsed.revision);
100
+ if (this.#coverageWorkspaces.has(key))
101
+ return "duplicate";
102
+ this.#coverageWorkspaces.set(key, parsed);
103
+ return "added";
104
+ }
105
+ reconstructionCoverageWorkspace(workspaceId, revision) {
106
+ const workspace = this.#coverageWorkspaces.get(workspaceKey(workspaceId, revision));
107
+ return workspace === undefined ? undefined : structuredClone(workspace);
108
+ }
109
+ reconstructionCoverageWorkspaces() {
110
+ return sortedWorkspaces(this.#coverageWorkspaces.values());
111
+ }
112
+ recordUnknown(input) {
113
+ const target = this.activeAnalysisBinding()?.target;
114
+ return this.#evidence.recordUnknown(input, unknownMutationEvidence(target, input));
115
+ }
116
+ recordEvidenceWithUnknown(evidence, input) {
117
+ return this.#evidence.recordWithUnknown(evidence, input, unknownMutationEvidence(undefined, input));
118
+ }
119
+ updateUnknown(input) {
120
+ const target = this.activeAnalysisBinding()?.target;
121
+ const evidence = createEvidence(target, UNKNOWN_REGISTRY_PROVIDER, {
122
+ predicateType: "rea.residual-unknown-mutation/v1",
123
+ operation: "update_unknown",
124
+ parameters: {
125
+ unknown_id: input.unknown_id,
126
+ expected_revision: input.expected_revision,
127
+ },
128
+ result: { action: "update", status: input.status },
129
+ confidence: "derived",
130
+ authority: "analyst-inference",
131
+ evidenceLinks: unknownEvidenceLinks(input),
132
+ limitations: [
133
+ "Registry mutation evidence records analyst intent, not proof of the answer.",
134
+ ],
135
+ });
136
+ return this.#evidence.updateUnknown(input, evidence);
137
+ }
138
+ listUnknowns(filters = {}) {
139
+ return this.#evidence.listUnknowns(filters);
140
+ }
141
+ verifyUnknownResolution(unknownId) {
142
+ return this.#evidence.verifyUnknownResolution(unknownId);
143
+ }
144
+ }
145
+ const workspaceKey = (workspaceId, revision) => `${workspaceId}:${String(revision)}`;
146
+ const sortedWorkspaces = (workspaces) => [...workspaces]
147
+ .sort((left, right) => left.workspace_id.localeCompare(right.workspace_id) ||
148
+ left.revision - right.revision)
149
+ .map((workspace) => structuredClone(workspace));