diff --git a/packages/boot/package.json b/packages/boot/package.json index 88f80edfc16..db0c04e4b79 100644 --- a/packages/boot/package.json +++ b/packages/boot/package.json @@ -19,9 +19,9 @@ "author": "Agoric", "license": "Apache-2.0", "dependencies": { + "@agoric/builders": "^0.1.0", "@agoric/ertp": "^0.16.2", "@agoric/internal": "^0.3.2", - "@agoric/builders": "^0.1.0", "@agoric/vat-data": "^0.5.2", "@agoric/vats": "^0.15.1", "@agoric/vm-config": "^0.1.0", @@ -48,7 +48,8 @@ "@agoric/swingset-vat": "^0.32.2", "@agoric/time": "^0.3.2", "ava": "^5.3.0", - "c8": "^7.13.0" + "c8": "^7.13.0", + "tsx": "^3.12.8" }, "files": [ "CHANGELOG.md", @@ -63,8 +64,17 @@ "node": ">=14.15.0" }, "ava": { + "extensions": { + "js": true, + "ts": "module" + }, "files": [ - "test/**/test-*.js" + "test/**/test-*.js", + "test/**/test-*.ts" + ], + "nodeArguments": [ + "--loader=tsx", + "--no-warnings" ], "require": [ "@endo/init/debug.js" diff --git a/packages/boot/test/bootstrapTests/bench-vaults-performance.js b/packages/boot/test/bootstrapTests/bench-vaults-performance.js index 5b54dd38a17..a7553154ae9 100644 --- a/packages/boot/test/bootstrapTests/bench-vaults-performance.js +++ b/packages/boot/test/bootstrapTests/bench-vaults-performance.js @@ -12,8 +12,8 @@ import engineGC from '@agoric/internal/src/lib-nodejs/engine-gc.js'; import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; import { makeAgoricNamesRemotesFromFakeStorage } from '@agoric/vats/tools/board-utils.js'; -import { makeSwingsetTestKit } from './supports.js'; -import { makeWalletFactoryDriver } from './drivers.js'; +import { makeSwingsetTestKit } from './supports.ts'; +import { makeWalletFactoryDriver } from './drivers.ts'; /** * @type {import('ava').TestFn< diff --git a/packages/boot/test/bootstrapTests/bench-vaults-stripped.js b/packages/boot/test/bootstrapTests/bench-vaults-stripped.js index 9f97b21aad7..0fa7cac3e7c 100644 --- a/packages/boot/test/bootstrapTests/bench-vaults-stripped.js +++ b/packages/boot/test/bootstrapTests/bench-vaults-stripped.js @@ -8,8 +8,8 @@ import { Offers } from '@agoric/inter-protocol/src/clientSupport.js'; import { eventLoopIteration } from '@agoric/internal/src/testing-utils.js'; import { makeAgoricNamesRemotesFromFakeStorage } from '@agoric/vats/tools/board-utils.js'; -import { makeSwingsetTestKit } from './supports.js'; -import { makeWalletFactoryDriver } from './drivers.js'; +import { makeSwingsetTestKit } from './supports.ts'; +import { makeWalletFactoryDriver } from './drivers.ts'; // presently all these tests use one collateral manager const collateralBrandKey = 'ATOM'; diff --git a/packages/boot/test/bootstrapTests/drivers.js b/packages/boot/test/bootstrapTests/drivers.ts similarity index 72% rename from packages/boot/test/bootstrapTests/drivers.js rename to packages/boot/test/bootstrapTests/drivers.ts index 2956d086dc2..72d3c5bacfd 100644 --- a/packages/boot/test/bootstrapTests/drivers.js +++ b/packages/boot/test/bootstrapTests/drivers.ts @@ -1,50 +1,55 @@ -// @ts-check +/* eslint-disable jsdoc/require-param */ import { Fail, NonNullish } from '@agoric/assert'; import { Offers } from '@agoric/inter-protocol/src/clientSupport.js'; import { SECONDS_PER_MINUTE } from '@agoric/inter-protocol/src/proposals/econ-behaviors.js'; import { unmarshalFromVstorage } from '@agoric/internal/src/marshal.js'; -import { slotToRemotable } from '@agoric/internal/src/storage-test-utils.js'; +import { + FakeStorageKit, + slotToRemotable, +} from '@agoric/internal/src/storage-test-utils.js'; import { instanceNameFor } from '@agoric/inter-protocol/src/proposals/price-feed-proposal.js'; -import { boardSlottingMarshaller } from '@agoric/vats/tools/board-utils.js'; +import { + AgoricNamesRemotes, + boardSlottingMarshaller, +} from '@agoric/vats/tools/board-utils.js'; +import type { + CurrentWalletRecord, + SmartWallet, + UpdateRecord, +} from '@agoric/smart-wallet/src/smartWallet.js'; +import type { WalletFactoryStartResult } from '@agoric/vats/src/core/startWalletFactory.js'; +import type { OfferSpec } from '@agoric/smart-wallet/src/offers.js'; +import type { TimerService } from '@agoric/time/src/types.js'; +import type { OfferMaker } from '@agoric/smart-wallet/src/types.js'; +import type { RunUtils, SwingsetTestKit } from './supports.ts'; -/** - * @param {ReturnType} runUtils - * @param {import('@agoric/internal/src/storage-test-utils.js').FakeStorageKit} storage - * @param {import('@agoric/vats/tools/board-utils.js').AgoricNamesRemotes} agoricNamesRemotes - */ export const makeWalletFactoryDriver = async ( - runUtils, - storage, - agoricNamesRemotes, + runUtils: RunUtils, + storage: FakeStorageKit, + agoricNamesRemotes: AgoricNamesRemotes, ) => { const { EV } = runUtils; - /** @type {import('@agoric/vats/src/core/startWalletFactory.js').WalletFactoryStartResult} */ - const walletFactoryStartResult = await EV.vat('bootstrap').consumeItem( - 'walletFactoryStartResult', - ); - /** @type {ERef} */ - const bankManager = await EV.vat('bootstrap').consumeItem('bankManager'); + const walletFactoryStartResult: WalletFactoryStartResult = await EV.vat( + 'bootstrap', + ).consumeItem('walletFactoryStartResult'); + const bankManager: ERef = + await EV.vat('bootstrap').consumeItem('bankManager'); const namesByAddressAdmin = await EV.vat('bootstrap').consumeItem( 'namesByAddressAdmin', ); const marshaller = boardSlottingMarshaller(slotToRemotable); - /** - * @param {string} walletAddress - * @param {import('@agoric/smart-wallet/src/smartWallet.js').SmartWallet} walletPresence - * @param {boolean} isNew - */ - const makeWalletDriver = (walletAddress, walletPresence, isNew) => ({ + const makeWalletDriver = ( + walletAddress: string, + walletPresence: SmartWallet, + isNew: boolean, + ) => ({ isNew, - /** - * @param {import('@agoric/smart-wallet/src/offers.js').OfferSpec} offer - * @returns {Promise} - */ - executeOffer(offer) { + executeOffer(offer: OfferSpec): Promise { const offerCapData = marshaller.toCapData( harden({ method: 'executeOffer', @@ -53,11 +58,7 @@ export const makeWalletFactoryDriver = async ( ); return EV(walletPresence).handleBridgeAction(offerCapData, true); }, - /** - * @param {import('@agoric/smart-wallet/src/offers.js').OfferSpec} offer - * @returns {Promise} - */ - sendOffer(offer) { + sendOffer(offer: OfferSpec): Promise { const offerCapData = marshaller.toCapData( harden({ method: 'executeOffer', @@ -67,8 +68,7 @@ export const makeWalletFactoryDriver = async ( return EV.sendOnly(walletPresence).handleBridgeAction(offerCapData, true); }, - /** @param {string} offerId */ - tryExitOffer(offerId) { + tryExitOffer(offerId: string) { const capData = marshaller.toCapData( harden({ method: 'tryExitOffer', @@ -77,33 +77,24 @@ export const makeWalletFactoryDriver = async ( ); return EV(walletPresence).handleBridgeAction(capData, true); }, - /** - * @template {import('@agoric/smart-wallet/src/types.js').OfferMaker} M - * offer maker function - * @param {M} makeOffer - * @param {Parameters[1]} firstArg - * @param {Parameters[2]} [secondArg] - * @returns {Promise} - */ - executeOfferMaker(makeOffer, firstArg, secondArg) { + executeOfferMaker( + makeOffer: M, + firstArg: Parameters[1], + secondArg?: Parameters[2], + ): Promise { const offer = makeOffer(agoricNamesRemotes, firstArg, secondArg); return this.executeOffer(offer); }, - /** - * @template {import('@agoric/smart-wallet/src/types.js').OfferMaker} M - * offer maker function - * @param {M} makeOffer - * @param {Parameters[1]} firstArg - * @param {Parameters[2]} [secondArg] - * @returns {Promise} - */ - sendOfferMaker(makeOffer, firstArg, secondArg) { + sendOfferMaker( + makeOffer: M, + firstArg: Parameters[1], + secondArg?: Parameters[2], + ): Promise { const offer = makeOffer(agoricNamesRemotes, firstArg, secondArg); return this.sendOffer(offer); }, - /** @returns {import('@agoric/smart-wallet/src/smartWallet.js').CurrentWalletRecord} */ - getCurrentWalletRecord() { + getCurrentWalletRecord(): CurrentWalletRecord { const fromCapData = (...args) => Reflect.apply(marshaller.fromCapData, marshaller, args); return unmarshalFromVstorage( @@ -114,8 +105,7 @@ export const makeWalletFactoryDriver = async ( ); }, - /** @returns {import('@agoric/smart-wallet/src/smartWallet.js').UpdateRecord} */ - getLatestUpdateRecord() { + getLatestUpdateRecord(): UpdateRecord { const fromCapData = (...args) => Reflect.apply(marshaller.fromCapData, marshaller, args); return unmarshalFromVstorage( @@ -130,11 +120,10 @@ export const makeWalletFactoryDriver = async ( return { /** * Skip the provisionPool for tests - * - * @param {string} walletAddress - * @returns {Promise>} */ - async provideSmartWallet(walletAddress) { + async provideSmartWallet( + walletAddress: string, + ): Promise> { const bank = await EV(bankManager).getBankForAddress(walletAddress); return EV(walletFactoryStartResult.creatorFacet) .provideSmartWallet(walletAddress, bank, namesByAddressAdmin) @@ -144,18 +133,15 @@ export const makeWalletFactoryDriver = async ( }, }; }; +export type WalletFactoryDriver = Awaited< + ReturnType +>; -/** - * @param {string} collateralBrandKey - * @param {import('@agoric/vats/tools/board-utils.js').AgoricNamesRemotes} agoricNamesRemotes - * @param {Awaited>} walletFactoryDriver - * @param {string[]} oracleAddresses - */ export const makePriceFeedDriver = async ( - collateralBrandKey, - agoricNamesRemotes, - walletFactoryDriver, - oracleAddresses, + collateralBrandKey: string, + agoricNamesRemotes: AgoricNamesRemotes, + walletFactoryDriver: WalletFactoryDriver, + oracleAddresses: string[], ) => { const priceFeedName = instanceNameFor(collateralBrandKey, 'USD'); @@ -185,8 +171,7 @@ export const makePriceFeedDriver = async ( // zero is the initial lastReportedRoundId so causes an error: cannot report on previous rounds let roundId = 1n; return { - /** @param {number} price */ - async setPrice(price) { + async setPrice(price: number) { await Promise.all( oracleWallets.map(w => w.executeOfferMaker( @@ -208,26 +193,17 @@ export const makePriceFeedDriver = async ( }; }; -/** @typedef {Awaited>} SwingsetTestKit */ - -/** - * @param {SwingsetTestKit} testKit - * @param {import('@agoric/vats/tools/board-utils.js').AgoricNamesRemotes} agoricNamesRemotes - * @param {Awaited>} walletFactoryDriver - * @param {string[]} committeeAddresses - */ export const makeGovernanceDriver = async ( - testKit, - agoricNamesRemotes, - walletFactoryDriver, - committeeAddresses, + testKit: SwingsetTestKit, + agoricNamesRemotes: AgoricNamesRemotes, + walletFactoryDriver: WalletFactoryDriver, + committeeAddresses: string[], ) => { const { EV } = testKit.runUtils; const charterMembershipId = 'charterMembership'; const committeeMembershipId = 'committeeMembership'; - /** @type {ERef} */ - const chainTimerService = + const chainTimerService: ERef = await EV.vat('bootstrap').consumeItem('chainTimerService'); let invitationsAccepted = false; @@ -327,12 +303,7 @@ export const makeGovernanceDriver = async ( }; return { - /** - * @param {Instance} instance - * @param {object} params - * @param {object} [path] - */ - async changeParams(instance, params, path) { + async changeParams(instance: Instance, params: Object, path?: object) { instance || Fail`missing instance`; await ensureInvitationsAccepted(); await proposeParams(instance, params, path); @@ -342,8 +313,7 @@ export const makeGovernanceDriver = async ( }; }; -/** @param {SwingsetTestKit} testKit */ -export const makeZoeDriver = async testKit => { +export const makeZoeDriver = async (testKit: SwingsetTestKit) => { const { EV } = testKit.runUtils; const zoe = await EV.vat('bootstrap').consumeItem('zoe'); const chainStorage = await EV.vat('bootstrap').consumeItem('chainStorage'); diff --git a/packages/boot/test/bootstrapTests/liquidation.js b/packages/boot/test/bootstrapTests/liquidation.ts similarity index 86% rename from packages/boot/test/bootstrapTests/liquidation.js rename to packages/boot/test/bootstrapTests/liquidation.ts index 6eabe517aec..3d54bb6dc58 100644 --- a/packages/boot/test/bootstrapTests/liquidation.js +++ b/packages/boot/test/bootstrapTests/liquidation.ts @@ -3,13 +3,16 @@ import { SECONDS_PER_HOUR, SECONDS_PER_MINUTE, } from '@agoric/inter-protocol/src/proposals/econ-behaviors.js'; -import { makeAgoricNamesRemotesFromFakeStorage } from '@agoric/vats/tools/board-utils.js'; +import { + AgoricNamesRemotes, + makeAgoricNamesRemotesFromFakeStorage, +} from '@agoric/vats/tools/board-utils.js'; import { makeGovernanceDriver, makePriceFeedDriver, makeWalletFactoryDriver, -} from './drivers.js'; -import { makeSwingsetTestKit } from './supports.js'; +} from './drivers.ts'; +import { makeSwingsetTestKit } from './supports.ts'; export const scale6 = x => BigInt(Math.round(x * 1_000_000)); @@ -39,10 +42,8 @@ export const makeLiquidationTestContext = async t => { console.timeLog('DefaultTestContext', 'vaultFactoryKit'); // has to be late enough for agoricNames data to have been published - /** @type {import('@agoric/vats/tools/board-utils.js').AgoricNamesRemotes} */ - const agoricNamesRemotes = makeAgoricNamesRemotesFromFakeStorage( - swingsetTestKit.storage, - ); + const agoricNamesRemotes: AgoricNamesRemotes = + makeAgoricNamesRemotesFromFakeStorage(swingsetTestKit.storage); const refreshAgoricNamesRemotes = () => { Object.assign( agoricNamesRemotes, @@ -72,24 +73,22 @@ export const makeLiquidationTestContext = async t => { ); console.timeLog('DefaultTestContext', 'governanceDriver'); - /** - * @type {Record< - * string, - * Awaited> - * >} - */ - const priceFeedDrivers = {}; + const priceFeedDrivers = {} as Record< + string, + Awaited> + >; console.timeLog('DefaultTestContext', 'priceFeedDriver'); console.timeEnd('DefaultTestContext'); - /** - * @param {object} opts - * @param {string} opts.collateralBrandKey - * @param {number} opts.managerIndex - */ - const setupStartingState = async ({ collateralBrandKey, managerIndex }) => { + const setupStartingState = async ({ + collateralBrandKey, + managerIndex, + }: { + collateralBrandKey: string; + managerIndex: number; + }) => { const managerPath = `published.vaultFactory.managers.manager${managerIndex}`; const { advanceTimeBy, readLatest } = swingsetTestKit; @@ -192,12 +191,11 @@ export const makeLiquidationTestContext = async t => { }; const check = { - /** - * @param {number} managerIndex - * @param {number} vaultIndex - * @param {Record} partial - */ - vaultNotification(managerIndex, vaultIndex, partial) { + vaultNotification( + managerIndex: number, + vaultIndex: number, + partial: Record, + ) { const { readLatest } = swingsetTestKit; const notification = readLatest( @@ -218,3 +216,7 @@ export const makeLiquidationTestContext = async t => { walletFactoryDriver, }; }; + +export type LiquidationTestContext = Awaited< + ReturnType +>; diff --git a/packages/boot/test/bootstrapTests/supports.js b/packages/boot/test/bootstrapTests/supports.ts similarity index 78% rename from packages/boot/test/bootstrapTests/supports.js rename to packages/boot/test/bootstrapTests/supports.ts index f1c8b4fef53..cbf2794cbf6 100644 --- a/packages/boot/test/bootstrapTests/supports.js +++ b/packages/boot/test/bootstrapTests/supports.ts @@ -1,43 +1,40 @@ -// @ts-check +/* eslint-disable jsdoc/require-param-type, jsdoc/require-param, @jessie.js/safe-await-separator */ /* global process */ +import childProcessAmbient from 'child_process'; import { promises as fsAmbientPromises } from 'fs'; import { resolve as importMetaResolve } from 'import-meta-resolve'; import { basename } from 'path'; import { inspect } from 'util'; -import childProcessAmbient from 'child_process'; -import { Fail } from '@agoric/assert'; +import { Fail, NonNullish } from '@agoric/assert'; import { buildSwingset } from '@agoric/cosmic-swingset/src/launch-chain.js'; -import { BridgeId, makeTracer, VBankAccount } from '@agoric/internal'; +import { BridgeId, VBankAccount, makeTracer } from '@agoric/internal'; import { unmarshalFromVstorage } from '@agoric/internal/src/marshal.js'; -import { makeFakeStorageKit } from '@agoric/internal/src/storage-test-utils.js'; +import { + FakeStorageKit, + makeFakeStorageKit, +} from '@agoric/internal/src/storage-test-utils.js'; import { initSwingStore } from '@agoric/swing-store'; import { kunser } from '@agoric/swingset-liveslots/test/kmarshal.js'; import { loadSwingsetConfigFile } from '@agoric/swingset-vat'; -import { E } from '@endo/eventual-send'; -import { makeQueue } from '@endo/stream'; -import { TimeMath } from '@agoric/time'; +import { TimeMath, Timestamp } from '@agoric/time'; import { boardSlottingMarshaller, slotToBoardRemote, } from '@agoric/vats/tools/board-utils.js'; +import { makeQueue } from '@endo/stream'; -// to retain for ESlint, used by typedef -E; +import type { SwingsetController } from '@agoric/swingset-vat/src/controller/controller.js'; +import type { BootstrapRootObject } from '@agoric/vats/src/core/lib-boot'; +import type { ExecutionContext } from 'ava'; +import type { E } from '@endo/eventual-send'; const sink = () => {}; const trace = makeTracer('BSTSupport', false); -/** - * @typedef {Awaited< - * ReturnType - * >} BootstrapRootObject - */ - -/** @type {{ [P in keyof BootstrapRootObject]: P }} */ -export const bootstrapMethods = { +export const bootstrapMethods: { [P in keyof BootstrapRootObject]: P } = { bootstrap: 'bootstrap', consumeItem: 'consumeItem', produceItem: 'produceItem', @@ -49,50 +46,41 @@ export const bootstrapMethods = { snapshotStore: 'snapshotStore', }; -/** - * @template {PropertyKey} K - * @template V - * @param {K[]} keys - * @param {(key: K, i: number) => V} valueMaker - */ -const keysToObject = (keys, valueMaker) => { +const keysToObject = ( + keys: K[], + valueMaker: (key: K, i: number) => V, +) => { return Object.fromEntries(keys.map((key, i) => [key, valueMaker(key, i)])); }; /** * AVA's default t.deepEqual() is nearly unreadable for sorted arrays of * strings. - * - * @param {{ - * deepEqual: (a: unknown, b: unknown, message?: string) => void; - * }} t - * @param {PropertyKey[]} a - * @param {PropertyKey[]} b - * @param {string} [message] */ -export const keyArrayEqual = (t, a, b, message) => { +export const keyArrayEqual = ( + t: ExecutionContext, + a: PropertyKey[], + b: PropertyKey[], + message?: string, +) => { const aobj = keysToObject(a, () => 1); const bobj = keysToObject(b, () => 1); return t.deepEqual(aobj, bobj, message); }; -/** - * @param {import('@agoric/swingset-vat/src/controller/controller').SwingsetController} controller - * @param {(..._: any[]) => any} log - */ -export const makeRunUtils = (controller, log = (..._) => {}) => { +export const makeRunUtils = ( + controller: SwingsetController, + log = (..._) => {}, +) => { let cranksRun = 0; const mutex = makeQueue(); mutex.put(controller.run()); - /** - * @template {() => any} T - * @param {T} thunk - * @returns {Promise>} - */ - const runThunk = async thunk => { + const runThunk = async any>( + thunk: T, + ): Promise> => { try { // this promise for the last lock may fail await mutex.get(); @@ -111,7 +99,7 @@ export const makeRunUtils = (controller, log = (..._) => {}) => { return result; }; - const runMethod = async (method, args = []) => { + const runMethod = async (method: string, args: object[] = []) => { log('runMethod', method, args, 'at', cranksRun); assert(Array.isArray(args)); @@ -132,15 +120,13 @@ export const makeRunUtils = (controller, log = (..._) => {}) => { } }; - /** - * @type {typeof E & { - * sendOnly: (presence: unknown) => Record void>; - * vat: (name: string) => Record Promise>; - * rawBoot: Record Promise>; - * }} - */ - // @ts-expect-error cast, approximate - const EV = presence => + type EVProxy = typeof E & { + sendOnly: (presence: unknown) => Record void>; + vat: (name: string) => Record Promise>; + rawBoot: Record Promise>; + }; + // @ts-expect-error XXX casting + const EV: EVProxy = presence => new Proxy(harden({}), { get: (_t, methodName, _rx) => harden((...args) => @@ -151,6 +137,7 @@ export const makeRunUtils = (controller, log = (..._) => {}) => { new Proxy(harden({}), { get: (_t, methodName, _rx) => harden((...args) => { + assert.string(methodName); if (name === 'meta') { return runMethod(methodName, args); } @@ -159,6 +146,7 @@ export const makeRunUtils = (controller, log = (..._) => {}) => { }); EV.rawBoot = new Proxy(harden({}), { get: (_t, methodName, _rx) => + // @ts-expect-error FIXME runMethod takes string but proxy allows symbol harden((...args) => runMethod(methodName, args)), }); // @ts-expect-error xxx @@ -174,7 +162,7 @@ export const makeRunUtils = (controller, log = (..._) => {}) => { ]), }, ); - // @ts-expect-error xxx + // @ts-expect-error 'get' is a read-only property EV.get = presence => new Proxy(harden({}), { get: (_t, pathElement, _rx) => @@ -183,6 +171,7 @@ export const makeRunUtils = (controller, log = (..._) => {}) => { return harden({ runThunk, EV }); }; +export type RunUtils = ReturnType; export const getNodeTestVaultsConfig = async ( bundleDir = 'bundles', @@ -191,10 +180,9 @@ export const getNodeTestVaultsConfig = async ( const fullPath = await importMetaResolve(specifier, import.meta.url).then( u => new URL(u).pathname, ); - const config = /** @type {SwingSetConfig & { coreProposals?: any[] }} */ ( - await loadSwingsetConfigFile(fullPath) + const config: SwingSetConfig & { coreProposals?: any[] } = NonNullish( + await loadSwingsetConfigFile(fullPath), ); - assert(config); // speed up (e.g. 80s vs 133s with xs-worker in production config) config.defaultManagerType = 'local'; @@ -218,12 +206,12 @@ export const getNodeTestVaultsConfig = async ( return testConfigPath; }; -/** - * @param {object} powers - * @param {Pick} powers.childProcess - * @param {typeof import('node:fs/promises')} powers.fs - */ -export const makeProposalExtractor = ({ childProcess, fs }) => { +interface Powers { + childProcess: Pick; + fs: typeof import('node:fs/promises'); +} + +export const makeProposalExtractor = ({ childProcess, fs }: Powers) => { const getPkgPath = (pkg, fileName = '') => new URL(`../../../${pkg}/${fileName}`, import.meta.url).pathname; @@ -240,8 +228,7 @@ export const makeProposalExtractor = ({ childProcess, fs }) => { harden(JSON.parse(await fs.readFile(filePath, 'utf8'))); // XXX parses the output to find the files but could write them to a path that can be traversed - /** @param {string} txt */ - const parseProposalParts = txt => { + const parseProposalParts = (txt: string) => { const evals = [ ...txt.matchAll(/swingset-core-eval (?\S+) (?