Files
proxmox/scripts/verify/check-gru-v2-deployment-queue.sh
defiQUG dbd517b279 Sync workspace: config, docs, scripts, CI, operator rules, and submodule pointers.
- Update dbis_core, cross-chain-pmm-lps, explorer-monorepo, metamask-integration, pr-workspace/chains
- Omit embedded publish git dirs and empty placeholders from index

Made-with: Cursor
2026-04-12 06:12:20 -07:00

490 lines
22 KiB
Bash

#!/usr/bin/env bash
# Generate an operator-grade GRU v2 public deployment queue across Wave 1
# transport activation, public-chain cW pool deployment, and protocol staging.
#
# Usage:
# bash scripts/verify/check-gru-v2-deployment-queue.sh
# bash scripts/verify/check-gru-v2-deployment-queue.sh --json
# bash scripts/verify/check-gru-v2-deployment-queue.sh --write-explorer-config
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
export PROJECT_ROOT
OUTPUT_JSON=0
WRITE_EXPLORER_CONFIG=0
for arg in "$@"; do
case "$arg" in
--json) OUTPUT_JSON=1 ;;
--write-explorer-config) WRITE_EXPLORER_CONFIG=1 ;;
*)
echo "Unknown argument: $arg" >&2
exit 2
;;
esac
done
command -v node >/dev/null 2>&1 || {
echo "[FAIL] Missing required command: node" >&2
exit 1
}
OUTPUT_JSON="$OUTPUT_JSON" WRITE_EXPLORER_CONFIG="$WRITE_EXPLORER_CONFIG" node <<'NODE'
const fs = require('fs');
const path = require('path');
const root = process.env.PROJECT_ROOT;
const outputJson = process.env.OUTPUT_JSON === '1';
const writeExplorerConfig = process.env.WRITE_EXPLORER_CONFIG === '1';
const explorerConfigPath = path.join(
root,
'explorer-monorepo/backend/api/rest/config/metamask/GRU_V2_DEPLOYMENT_QUEUE.json'
);
function readJson(relPath) {
return JSON.parse(fs.readFileSync(path.join(root, relPath), 'utf8'));
}
const rollout = readJson('config/gru-global-priority-currency-rollout.json');
const manifest = readJson('config/gru-iso4217-currency-manifest.json');
const transport = readJson('config/gru-transport-active.json');
const deployment = readJson('cross-chain-pmm-lps/config/deployment-status.json');
const poolMatrix = readJson('cross-chain-pmm-lps/config/pool-matrix.json');
const protocolPlan = readJson('config/gru-v2-public-protocol-rollout-plan.json');
const mapping = readJson('config/token-mapping-multichain.json');
const routingRegistry = readJson('config/routing-registry.json');
const manifestByCode = new Map((manifest.currencies || []).map((item) => [item.code, item]));
const transportSymbols = new Set((transport.enabledCanonicalTokens || []).map((item) => item.symbol));
const desiredChainIds = rollout.desiredDestinationNetworks?.evmPublicCwMeshChainIds || [];
const poolMatrixTokens = new Set(poolMatrix.cwTokens || []);
const cToCw = mapping.cToCwSymbolMapping || {};
const wave1Assets = (rollout.assets || []).filter((asset) => asset.wave === 'wave1');
const wave1WrappedSymbols = [...new Set(wave1Assets.flatMap((asset) => (asset.tokenForms || []).map((item) => item.wrappedSymbol)))];
const wave1CanonicalSymbols = [...new Set(wave1Assets.flatMap((asset) => (asset.tokenForms || []).map((item) => item.canonicalSymbol)))];
const poolMatrixMissingWave1 = wave1WrappedSymbols.filter((symbol) => !poolMatrixTokens.has(symbol));
const arbitrumRoute = (routingRegistry.routes || []).find((route) => route.fromChain === 138 && route.toChain === 42161 && route.asset === 'WETH');
const arbitrumHubBlocker = {
active: true,
fromChain: 138,
viaChain: 1,
toChain: 42161,
currentPath: '138 -> Mainnet -> Arbitrum',
sourceBridge: '0xc9901ce2Ddb6490FAA183645147a87496d8b20B6',
failedTxHash: '0x97df657f0e31341ca852666766e553650531bbcc86621246d041985d7261bb07',
note: (arbitrumRoute && arbitrumRoute.note) || 'Use Mainnet hub; the current Mainnet -> Arbitrum WETH9 leg is blocked.'
};
function deriveRepoState(bits) {
if (bits.transportActive) return 'live_transport';
if (bits.canonical138Deployed) return 'canonical_only';
if (bits.manifestPresent) return 'manifest_only';
if (bits.cToCwMapped) return 'mapping_only';
return 'backlog';
}
const allAssetResults = (rollout.assets || []).map((asset) => {
const manifestEntry = manifestByCode.get(asset.code);
const canonicalSymbols = (asset.tokenForms || []).map((item) => item.canonicalSymbol);
const wrappedSymbols = (asset.tokenForms || []).map((item) => item.wrappedSymbol);
const manifestPresent = Boolean(manifestEntry);
const canonical138Deployed = Boolean(manifestEntry?.status?.deployed);
const transportActive = canonicalSymbols.some((symbol) => transportSymbols.has(symbol));
const cToCwMapped = canonicalSymbols.length > 0 && canonicalSymbols.every((symbol, idx) => cToCw[symbol] === wrappedSymbols[idx]);
return {
code: asset.code,
wave: asset.wave,
currentRepoState: deriveRepoState({
manifestPresent,
canonical138Deployed,
cToCwMapped,
transportActive
})
};
});
const rolloutBacklogAssets = allAssetResults.filter((item) => item.currentRepoState === 'backlog').length;
function normalizePair(pair) {
return String(pair || '').trim().toUpperCase();
}
function poolEntryMatchesPair(entry, pair) {
const normalized = normalizePair(pair);
const [base, quote] = normalized.split('/');
const baseCandidate = String(entry.base || entry.base_token || '').trim().toUpperCase();
const quoteCandidate = String(entry.quote || entry.quote_token || '').trim().toUpperCase();
return baseCandidate === base && quoteCandidate === quote;
}
const assetQueue = wave1Assets.map((asset) => {
const manifestEntry = manifestByCode.get(asset.code);
const canonicalSymbols = (asset.tokenForms || []).map((item) => item.canonicalSymbol);
const wrappedSymbols = (asset.tokenForms || []).map((item) => item.wrappedSymbol);
const transportActive = canonicalSymbols.some((symbol) => transportSymbols.has(symbol));
const coveredByPoolMatrix = wrappedSymbols.every((symbol) => poolMatrixTokens.has(symbol));
return {
code: asset.code,
name: asset.name,
canonicalSymbols,
wrappedSymbols,
transportActive,
canonicalDeployed: Boolean(manifestEntry?.status?.deployed),
x402Ready: Boolean(manifestEntry?.status?.x402Ready),
coveredByPoolMatrix,
nextSteps: transportActive
? ['monitor_live_transport', 'deploy_public_pools']
: ['enable_bridge_controls', 'set_max_outstanding', 'promote_transport_overlay', 'deploy_public_pools']
};
});
const chainQueue = desiredChainIds.map((chainId) => {
const chain = deployment.chains?.[String(chainId)] || {};
const matrix = poolMatrix.chains?.[String(chainId)] || {};
const cwTokens = Object.keys(chain.cwTokens || {});
const pmmPools = Array.isArray(chain.pmmPools) ? chain.pmmPools : [];
const plannedWave1Pairs = (matrix.poolsFirst || []).filter((pair) => {
return wave1WrappedSymbols.some((symbol) => normalizePair(pair).startsWith(`${symbol.toUpperCase()}/`));
});
const recordedWave1Pairs = plannedWave1Pairs.filter((pair) => pmmPools.some((entry) => poolEntryMatchesPair(entry, pair)));
return {
chainId,
name: matrix.name || chain.name || `Chain ${chainId}`,
hubStable: matrix.hubStable || null,
bridgeAvailable: chain.bridgeAvailable === true,
cwTokenCount: cwTokens.length,
wave1WrappedCoverage: wave1WrappedSymbols.filter((symbol) => cwTokens.includes(symbol)).length,
plannedWave1Pairs,
recordedWave1Pairs,
nextStep: cwTokens.length === 0
? 'complete_cw_suite_then_deploy_pools'
: recordedWave1Pairs.length === plannedWave1Pairs.length && plannedWave1Pairs.length > 0
? 'verify_and_route'
: 'deploy_first_tier_wave1_pools'
};
});
const totalRecordedPublicPools = desiredChainIds.reduce((sum, chainId) => {
const chain = deployment.chains?.[String(chainId)] || {};
const pmmPools = Array.isArray(chain.pmmPools) ? chain.pmmPools : [];
return sum + pmmPools.length;
}, 0);
const protocolQueue = (protocolPlan.protocols || []).map((protocol) => {
if (protocol.key === 'dodo_v3_d3mm') {
return {
key: protocol.key,
name: protocol.name,
role: protocol.role,
deploymentStage: protocol.deploymentStage,
activePublicPools: 0,
currentState: 'pilot_live_chain138_only',
activationDependsOn: protocol.activationDependsOn || []
};
}
if (protocol.key === 'dodo_pmm') {
return {
key: protocol.key,
name: protocol.name,
role: protocol.role,
deploymentStage: protocol.deploymentStage,
activePublicPools: totalRecordedPublicPools,
currentState: totalRecordedPublicPools > 0 ? 'partially_live_on_public_cw_mesh' : 'queued_not_live',
activationDependsOn: protocol.activationDependsOn || []
};
}
return {
key: protocol.key,
name: protocol.name,
role: protocol.role,
deploymentStage: protocol.deploymentStage,
activePublicPools: 0,
currentState: 'queued_not_live',
activationDependsOn: protocol.activationDependsOn || []
};
});
const summary = {
wave1Assets: assetQueue.length,
wave1TransportActive: assetQueue.filter((item) => item.transportActive).length,
wave1TransportPending: assetQueue.filter((item) => !item.transportActive).length,
wave1WrappedSymbols: wave1WrappedSymbols.length,
wave1WrappedSymbolsCoveredByPoolMatrix: wave1WrappedSymbols.length - poolMatrixMissingWave1.length,
wave1WrappedSymbolsMissingFromPoolMatrix: poolMatrixMissingWave1.length,
desiredPublicEvmTargets: chainQueue.length,
chainsWithLoadedCwSuites: chainQueue.filter((item) => item.cwTokenCount > 0).length,
chainsMissingCwSuites: chainQueue.filter((item) => item.cwTokenCount === 0).length,
firstTierWave1PoolsPlanned: chainQueue.reduce((sum, item) => sum + item.plannedWave1Pairs.length, 0),
firstTierWave1PoolsRecordedLive: chainQueue.reduce((sum, item) => sum + item.recordedWave1Pairs.length, 0),
protocolsTracked: protocolQueue.length,
protocolsLive: protocolQueue.filter((item) => item.activePublicPools > 0).length
};
const blockers = [];
if (poolMatrixMissingWave1.length > 0) {
blockers.push(`Wave 1 wrapped symbols missing from pool-matrix: ${poolMatrixMissingWave1.join(', ')}.`);
}
const missingSuiteChains = chainQueue.filter((item) => item.cwTokenCount === 0);
if (missingSuiteChains.length > 0) {
blockers.push(`Desired public EVM targets still missing cW suites: ${missingSuiteChains.map((item) => item.name).join(', ')}.`);
}
const pendingWave1 = assetQueue.filter((item) => !item.transportActive);
if (pendingWave1.length > 0) {
blockers.push(`Wave 1 transport is still pending for: ${pendingWave1.map((item) => item.code).join(', ')}.`);
}
if (summary.firstTierWave1PoolsRecordedLive === 0) {
blockers.push('No first-tier Wave 1 public cW pools are recorded live yet across the tracked public EVM mesh.');
}
if (protocolQueue.every((item) => item.activePublicPools === 0)) {
blockers.push('All tracked public protocols remain queued: Uniswap v3, DODO PMM, Balancer, Curve 3, and 1inch.');
}
if (arbitrumHubBlocker.active) {
blockers.push(`Arbitrum bootstrap remains blocked on the current Mainnet hub leg: tx ${arbitrumHubBlocker.failedTxHash} reverted before any bridge event was emitted.`);
}
const resolutionMatrix = [
{
key: 'mainnet_arbitrum_hub_blocked',
state: arbitrumHubBlocker.active ? 'open' : 'resolved',
blocker: arbitrumHubBlocker.active
? `Arbitrum bootstrap remains blocked on the current Mainnet hub leg: tx ${arbitrumHubBlocker.failedTxHash} reverted from ${arbitrumHubBlocker.sourceBridge} before any bridge event was emitted.`
: 'The Mainnet -> Arbitrum WETH9 hub leg is healthy.',
targets: [
{
fromChain: arbitrumHubBlocker.fromChain,
viaChain: arbitrumHubBlocker.viaChain,
toChain: arbitrumHubBlocker.toChain,
currentPath: arbitrumHubBlocker.currentPath
}
],
resolution: [
'Repair or replace the current Mainnet WETH9 fan-out bridge before treating Arbitrum as an available public bootstrap target.',
'Retest 138 -> Mainnet first-hop delivery, then rerun a smaller Mainnet -> Arbitrum send and require destination bridge events before promoting the route.',
'Keep Arbitrum marked blocked in the explorer and status surfaces until the hub leg emits and completes normally.'
],
runbooks: [
'docs/07-ccip/CROSS_NETWORK_FUNDING_BOOTSTRAP_STRATEGY.md',
'docs/07-ccip/CHAIN138_PUBLIC_CHAIN_UNLOAD_ROUTES.md',
'docs/00-meta/REQUIRED_FIXES_GAPS_AND_DEPLOYMENTS_LIST.md'
],
exitCriteria: 'A fresh Mainnet -> Arbitrum WETH9 send emits bridge events and completes destination delivery successfully.'
},
{
key: 'missing_public_cw_suites',
state: missingSuiteChains.length === 0 ? 'resolved' : 'open',
blocker: missingSuiteChains.length === 0
? 'All desired public EVM targets have cW suites.'
: `Desired public EVM targets still missing cW suites: ${missingSuiteChains.map((item) => item.name).join(', ')}.`,
targets: missingSuiteChains.map((item) => ({
chainId: item.chainId,
name: item.name,
nextStep: item.nextStep
})),
resolution: [
'Deploy the full cW core suite on each missing destination chain using the existing CW deploy-and-wire flow.',
'Grant bridge mint/burn roles and mark the corridor live in cross-chain-pmm-lps/config/deployment-status.json.',
'Update public token lists / explorer config, then rerun check-cw-evm-deployment-mesh.sh and check-cw-public-pool-status.sh.'
],
runbooks: [
'docs/07-ccip/CW_DEPLOY_AND_WIRE_RUNBOOK.md',
'docs/03-deployment/PHASE_C_CW_AND_EDGE_POOLS_RUNBOOK.md',
'scripts/deployment/run-cw-remaining-steps.sh',
'scripts/verify/check-cw-evm-deployment-mesh.sh'
],
exitCriteria: missingSuiteChains.length === 0
? 'All desired public EVM targets report non-zero cW suites and bridgeAvailable=true in deployment-status.json.'
: `${missingSuiteChains.map((item) => item.name).join(', ')} report non-zero cW suites and become bridgeAvailable in deployment-status.json.`
},
{
key: 'wave1_transport_pending',
state: pendingWave1.length === 0 ? 'resolved' : 'open',
blocker: pendingWave1.length === 0
? 'Wave 1 transport is fully active.'
: `Wave 1 transport is still pending for: ${pendingWave1.map((item) => item.code).join(', ')}.`,
targets: pendingWave1.map((item) => ({
code: item.code,
canonicalSymbols: item.canonicalSymbols,
wrappedSymbols: item.wrappedSymbols
})),
resolution: [
'Enable bridge controls and supervision policy for each Wave 1 canonical asset on Chain 138.',
'Set max-outstanding / capacity controls, then promote the canonical symbols into config/gru-transport-active.json.',
'Verify the overlay promotion with check-gru-global-priority-rollout.sh and check-gru-v2-chain138-readiness.sh before attaching public liquidity.'
],
runbooks: [
'docs/04-configuration/GRU_GLOBAL_PRIORITY_CROSS_CHAIN_ROLLOUT.md',
'docs/04-configuration/GRU_TRANSPORT_ACTIVE_JSON.md',
'scripts/verify/check-gru-global-priority-rollout.sh',
'scripts/verify/check-gru-v2-chain138-readiness.sh'
],
exitCriteria: 'Wave 1 transport pending count reaches zero and the overlay reports the seven non-USD assets as live_transport.'
},
{
key: 'first_tier_public_pools_not_live',
state: summary.firstTierWave1PoolsRecordedLive > 0 ? 'in_progress' : 'open',
blocker: summary.firstTierWave1PoolsRecordedLive > 0
? 'Some first-tier Wave 1 public cW pools are live, but the rollout is incomplete.'
: 'No first-tier Wave 1 public cW pools are recorded live yet across the tracked public EVM mesh.',
targets: chainQueue.map((item) => ({
chainId: item.chainId,
name: item.name,
hubStable: item.hubStable,
plannedWave1Pairs: item.plannedWave1Pairs.length,
recordedWave1Pairs: item.recordedWave1Pairs.length
})),
resolution: [
'Deploy the first-tier cW/hub-stable pairs from pool-matrix.json on every chain with a loaded cW suite.',
'Seed the new pools with initial liquidity and record the resulting pool addresses in cross-chain-pmm-lps/config/deployment-status.json.',
'Use check-cw-public-pool-status.sh to verify the mesh is no longer empty before surfacing the venues publicly.'
],
runbooks: [
'docs/03-deployment/SINGLE_SIDED_LPS_PUBLIC_NETWORKS_RUNBOOK.md',
'docs/03-deployment/PMM_FULL_MESH_AND_PUBLIC_SINGLE_SIDED_PLAN.md',
'cross-chain-pmm-lps/config/pool-matrix.json',
'scripts/verify/check-cw-public-pool-status.sh'
],
exitCriteria: 'First-tier Wave 1 pools are recorded live in deployment-status.json and check-cw-public-pool-status.sh reports non-zero pool coverage.'
},
{
key: 'public_protocols_queued',
state: protocolQueue.every((item) => item.activePublicPools === 0) ? 'open' : 'in_progress',
blocker: protocolQueue.every((item) => item.activePublicPools === 0)
? 'All tracked public protocols remain queued: Uniswap v3, DODO PMM, Balancer, Curve 3, and 1inch.'
: 'Some tracked public protocols have begun activation, but the full protocol stack is not live yet.',
targets: protocolQueue.map((item) => ({
key: item.key,
name: item.name,
deploymentStage: item.deploymentStage,
activationDependsOn: item.activationDependsOn
})),
resolution: [
'Stage 1: activate Uniswap v3 and DODO PMM once first-tier cW pools exist on the public mesh.',
'Stage 2: activate Balancer and Curve 3 only after first-tier stable liquidity is already live.',
'Stage 3: expose 1inch after the underlying pools, routing/indexer visibility, and public provider-capability wiring are in place.'
],
runbooks: [
'config/gru-v2-public-protocol-rollout-plan.json',
'docs/11-references/GRU_V2_PUBLIC_PROTOCOL_DEPLOYMENT_STATUS.md',
'scripts/verify/check-gru-v2-public-protocols.sh'
],
exitCriteria: 'The public protocol status surface reports non-zero active cW pools for the staged venues.'
},
{
key: 'global_priority_backlog',
state: rolloutBacklogAssets === 0 ? 'resolved' : 'open',
blocker: rolloutBacklogAssets === 0
? 'No ranked GRU backlog assets remain outside the live manifest.'
: `The ranked GRU global rollout still has ${rolloutBacklogAssets} backlog assets outside the live manifest.`,
targets: [
{ backlogAssets: rolloutBacklogAssets }
],
resolution: [
'Complete Wave 1 transport and first-tier public liquidity before promoting the remaining ranked assets.',
'For each backlog asset, add canonical + wrapped symbols to the manifest/rollout plan, deploy contracts, and extend the public pool matrix.',
'Promote each new asset through the same transport and public-liquidity gates used for Wave 1.'
],
runbooks: [
'config/gru-global-priority-currency-rollout.json',
'config/gru-iso4217-currency-manifest.json',
'docs/04-configuration/GRU_GLOBAL_PRIORITY_CROSS_CHAIN_ROLLOUT.md',
'scripts/verify/check-gru-global-priority-rollout.sh'
],
exitCriteria: 'Backlog assets count reaches zero in check-gru-global-priority-rollout.sh.'
},
{
key: 'solana_non_evm_program',
state: ((rollout.desiredDestinationNetworks?.nonEvmRelayPrograms || []).length || 0) === 0 ? 'resolved' : 'planned',
blocker: ((rollout.desiredDestinationNetworks?.nonEvmRelayPrograms || []).length || 0) === 0
? 'No desired non-EVM GRU targets remain.'
: `Desired non-EVM GRU targets remain planned / relay-dependent: ${(rollout.desiredDestinationNetworks.nonEvmRelayPrograms || []).map((item) => item.identifier).join(', ')}.`,
targets: (rollout.desiredDestinationNetworks?.nonEvmRelayPrograms || []).map((item) => ({
identifier: item.identifier,
label: item.label || item.identifier
})),
resolution: [
'Define the destination-chain token/program model first: SPL or wrapped-account representation, authority model, and relay custody surface.',
'Implement the relay/program path and only then promote Solana from desired-target status into the active transport inventory.',
'Add dedicated verifier coverage before marking Solana live anywhere in the explorer or status docs.'
],
runbooks: [
'docs/04-configuration/ADDITIONAL_PATHS_AND_EXTENSIONS.md',
'docs/04-configuration/GRU_GLOBAL_PRIORITY_CROSS_CHAIN_ROLLOUT.md'
],
exitCriteria: 'Solana has a real relay/program surface, a verifier, and is no longer only listed as a desired non-EVM target.'
}
];
const report = {
generatedAt: new Date().toISOString(),
summary,
assetQueue,
chainQueue,
protocolQueue,
blockers,
resolutionMatrix,
notes: [
'This queue is an operator/deployment planning surface. It does not mark queued pools or transports as live.',
'Chain 138 canonical venues remain a separate live surface from the public cW mesh.'
]
};
if (writeExplorerConfig) {
fs.writeFileSync(explorerConfigPath, `${JSON.stringify(report, null, 2)}\n`);
}
if (outputJson) {
console.log(JSON.stringify(report, null, 2));
process.exit(0);
}
console.log('=== GRU V2 Deployment Queue ===');
console.log(`Wave 1 assets: ${summary.wave1Assets}`);
console.log(`Wave 1 transport active: ${summary.wave1TransportActive}`);
console.log(`Wave 1 transport pending: ${summary.wave1TransportPending}`);
console.log(`Wave 1 wrapped symbols covered by pool-matrix: ${summary.wave1WrappedSymbolsCoveredByPoolMatrix}/${summary.wave1WrappedSymbols}`);
console.log(`Desired public EVM targets: ${summary.desiredPublicEvmTargets}`);
console.log(`Chains with loaded cW suites: ${summary.chainsWithLoadedCwSuites}`);
console.log(`Chains missing cW suites: ${summary.chainsMissingCwSuites}`);
console.log(`First-tier Wave 1 pools planned: ${summary.firstTierWave1PoolsPlanned}`);
console.log(`First-tier Wave 1 pools recorded live: ${summary.firstTierWave1PoolsRecordedLive}`);
console.log(`Tracked protocols: ${summary.protocolsTracked}`);
console.log('');
console.log('Wave 1 asset queue:');
for (const asset of assetQueue) {
console.log(`- ${asset.code}: transport=${asset.transportActive ? 'live' : 'pending'}; pool-matrix=${asset.coveredByPoolMatrix ? 'covered' : 'missing'}; next=${asset.nextSteps.join(',')}`);
}
console.log('');
console.log('Per-chain queue:');
for (const chain of chainQueue) {
console.log(`- ${chain.chainId} ${chain.name}: hub=${chain.hubStable || 'n/a'}; cw=${chain.cwTokenCount}; plannedWave1Pairs=${chain.plannedWave1Pairs.length}; liveWave1Pairs=${chain.recordedWave1Pairs.length}; next=${chain.nextStep}`);
}
console.log('');
console.log('Protocol queue:');
for (const protocol of protocolQueue) {
console.log(`- ${protocol.name}: ${protocol.currentState}; stage=${protocol.deploymentStage}`);
}
if (blockers.length > 0) {
console.log('');
console.log('Active blockers:');
for (const blocker of blockers) {
console.log(`- ${blocker}`);
}
console.log('');
console.log('Resolution paths:');
for (const entry of resolutionMatrix) {
if (entry.state === 'resolved') continue;
console.log(`- ${entry.key}: ${entry.exitCriteria}`);
}
}
if (writeExplorerConfig) {
console.log('');
console.log(`Wrote: ${explorerConfigPath}`);
}
NODE