From 6db45b4d2b52f8e2e780e74d8e5182f8dcad3c05 Mon Sep 17 00:00:00 2001 From: defiQUG Date: Tue, 28 Apr 2026 19:38:54 -0700 Subject: [PATCH] Add ALL Mainnet readiness gate generator --- package.json | 3 +- .../status/generate-all-mainnet-readiness.mjs | 388 ++++++++++++++++++ .../check-allmainnet-protocol-surface.sh | 32 +- 3 files changed, 421 insertions(+), 2 deletions(-) create mode 100755 scripts/status/generate-all-mainnet-readiness.mjs diff --git a/package.json b/package.json index 3c46c27d..443fdf5b 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,8 @@ "mission-control:start": "pnpm --filter mission-control start", "mission-control:test": "pnpm --filter mission-control test", "token-lists:validate-metadata": "node scripts/validation/validate-token-list-metadata.mjs", - "pool-matrix:validate": "node scripts/validation/validate-pool-creation-matrix.mjs" + "pool-matrix:validate": "node scripts/validation/validate-pool-creation-matrix.mjs", + "all-mainnet:readiness": "node scripts/status/generate-all-mainnet-readiness.mjs" }, "keywords": [ "proxmox", diff --git a/scripts/status/generate-all-mainnet-readiness.mjs b/scripts/status/generate-all-mainnet-readiness.mjs new file mode 100755 index 00000000..a9cdd812 --- /dev/null +++ b/scripts/status/generate-all-mainnet-readiness.mjs @@ -0,0 +1,388 @@ +#!/usr/bin/env node +/** + * Generate ALL Mainnet readiness reports from the canonical matrix and surface. + * + * The reports are intentionally conservative: rows are not promoted by this + * script. It only summarizes what is already proven in config and what remains + * gated by missing addresses, vault assignments, funding, canaries, or protocol + * surface evidence. + */ + +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { resolve } from "node:path"; + +const repoRoot = resolve(new URL("../..", import.meta.url).pathname); +const matrixPath = resolve(repoRoot, "config/all-mainnet-pool-creation-matrix.json"); +const surfacePath = resolve(repoRoot, "config/allmainnet-non-dodo-protocol-surface.json"); +const outDir = resolve(repoRoot, "reports/status"); + +const statusesReadyForFunding = new Set(["created", "funded", "live_read", "canary_passed", "production"]); +const statusesReadyForCanary = new Set(["funded", "live_read", "canary_passed", "production"]); +const productionStatuses = new Set(["production"]); + +function readJson(path) { + return JSON.parse(readFileSync(path, "utf8")); +} + +function countBy(items, keyFn) { + const counts = {}; + for (const item of items) { + const key = keyFn(item); + counts[key] = (counts[key] || 0) + 1; + } + return Object.fromEntries(Object.entries(counts).sort(([a], [b]) => a.localeCompare(b))); +} + +function tokenRef(token) { + if (!token) return "MISSING"; + return `${token.symbol || "MISSING"} ${token.address || "MISSING"}`; +} + +function rowRef(row) { + return { + poolId: row.poolId, + chainId: row.chainId, + network: row.network, + protocol: row.protocol, + status: row.status, + requiredForSpend: Boolean(row.requiredForSpend), + publicRoutingEnabled: Boolean(row.publicRoutingEnabled), + baseToken: row.baseToken, + quoteToken: row.quoteToken, + poolAddress: row.poolAddress, + vaultAddress: row.vaultAddress, + missingRequiredVaultRoles: row.missingRequiredVaultRoles || [], + canaryEvidence: row.canaryEvidence, + notes: row.notes || [], + }; +} + +function rowBlockers(row, surface) { + const blockers = []; + if (row.status === "planned") blockers.push("pool_not_created"); + if (!row.baseToken?.address) blockers.push(`missing_base_address:${row.baseToken?.symbol || "unknown"}`); + if (!row.quoteToken?.address) blockers.push(`missing_quote_address:${row.quoteToken?.symbol || "unknown"}`); + if (statusesReadyForFunding.has(row.status) && !row.poolAddress) blockers.push("missing_pool_address"); + if ((row.missingRequiredVaultRoles || []).length > 0) blockers.push("missing_required_vault_assignments"); + if (row.status === "created") blockers.push("pool_created_but_not_funded"); + if (statusesReadyForCanary.has(row.status) && !row.canaryEvidence) blockers.push("missing_canary_evidence"); + if (!productionStatuses.has(row.status)) blockers.push("not_production_status"); + if (row.chainId === 651940 && surface.summary?.sameChainSwapInventoryPublished !== true) { + blockers.push("all_mainnet_same_chain_inventory_not_published"); + } + return blockers; +} + +function makeSummary(matrix, surface) { + const rows = matrix.rows; + const requiredRows = rows.filter((row) => row.requiredForSpend === true); + const rowsMissingVaults = rows.filter((row) => (row.missingRequiredVaultRoles || []).length > 0); + const requiredRowsMissingVaults = requiredRows.filter((row) => (row.missingRequiredVaultRoles || []).length > 0); + const rowsMissingTokenAddresses = rows.filter((row) => !row.baseToken?.address || !row.quoteToken?.address); + const requiredRowsMissingTokenAddresses = requiredRows.filter((row) => !row.baseToken?.address || !row.quoteToken?.address); + const rowsMissingCanary = requiredRows.filter((row) => statusesReadyForCanary.has(row.status) && !row.canaryEvidence); + const productionRows = requiredRows.filter((row) => row.status === "production"); + + return { + generatedAt: new Date().toISOString(), + matrixVersion: matrix.version, + matrixGeneratedAt: matrix.generatedAt, + totalRows: rows.length, + requiredForSpendRows: requiredRows.length, + statusCounts: countBy(rows, (row) => row.status), + protocolCounts: countBy(rows, (row) => row.protocol), + chainStatusCounts: Object.fromEntries( + Object.entries(countBy(rows, (row) => `${row.chainId} ${row.network}`)).sort(([a], [b]) => a.localeCompare(b)), + ), + requiredStatusCounts: countBy(requiredRows, (row) => row.status), + plannedRows: rows.filter((row) => row.status === "planned").length, + createdUnfundedRows: rows.filter((row) => row.status === "created").length, + fundedRows: rows.filter((row) => row.status === "funded").length, + liveReadRows: rows.filter((row) => row.status === "live_read").length, + productionRows: productionRows.length, + rowsMissingVaultAssignments: rowsMissingVaults.length, + requiredRowsMissingVaultAssignments: requiredRowsMissingVaults.length, + rowsMissingTokenAddresses: rowsMissingTokenAddresses.length, + requiredRowsMissingTokenAddresses: requiredRowsMissingTokenAddresses.length, + requiredRowsMissingCanaryEvidence: rowsMissingCanary.length, + bridgeOnlyLive: surface.summary?.bridgeOnlyLive === true, + sameChainSwapInventoryPublished: surface.summary?.sameChainSwapInventoryPublished === true, + productionReady: + requiredRows.length > 0 && + productionRows.length === requiredRows.length && + requiredRowsMissingVaults.length === 0 && + rowsMissingCanary.length === 0 && + surface.summary?.sameChainSwapInventoryPublished === true, + }; +} + +function makeReadiness(matrix, surface, summary) { + const requiredRows = matrix.rows.filter((row) => row.requiredForSpend === true); + const allBlockers = requiredRows + .map((row) => ({ + ...rowRef(row), + blockers: rowBlockers(row, surface), + })) + .filter((row) => row.blockers.length > 0); + + const protocolSurfaceBlockers = (surface.protocols || []) + .filter((protocol) => protocol.status !== "live" && (!protocol.factoryAddress || !protocol.routerAddress)) + .map((protocol) => ({ + name: protocol.name, + family: protocol.family, + status: protocol.status, + factoryAddress: protocol.factoryAddress, + routerAddress: protocol.routerAddress, + blockers: [ + !protocol.factoryAddress ? "missing_factory_address" : null, + !protocol.routerAddress ? "missing_router_address" : null, + protocol.status !== "live" ? "protocol_surface_not_live" : null, + ].filter(Boolean), + })); + + return { + generatedAt: summary.generatedAt, + status: summary.productionReady ? "production_ready" : "blocked", + summary, + executionOrder: [ + "commit_or_confirm_missing_token_and_accounting_addresses", + "assign_required_vaults_and_pause_controls", + "create_remaining_planned_pools", + "fund_created_and_new_pools", + "run_live_reserve_reads", + "run_bridge_fee_and_destination_settlement_preflights", + "run_10_100_1000_canary_swaps", + "publish_same_chain_inventory_and_verification_artifacts", + "enable_public_routing_only_for_canary_passed_or_production_rows", + ], + blockers: allBlockers, + protocolSurfaceBlockers, + externalActionsRequired: [ + "operator_signing_required_for_pool_creation_or_funding", + "vault_addresses_required_from_treasury_or_security_owner", + "canary_swap_transactions_required_on_live_networks", + "same_chain_factory_router_pool_inventory_required_before_public_route_generation", + ], + }; +} + +function makeProductionGate(summary, readiness, surface) { + return { + generatedAt: summary.generatedAt, + status: summary.productionReady ? "production_ready" : "blocked", + gates: { + bridgeConfigOk: surface.bridgeSurface?.adapter?.status === "live", + sameChainSwapInventoryPublished: summary.sameChainSwapInventoryPublished, + requiredPoolsCreated: summary.requiredStatusCounts.planned === undefined, + requiredPoolsFundedOrBetter: !["planned", "created"].some((status) => summary.requiredStatusCounts[status] > 0), + vaultAssignmentsComplete: summary.requiredRowsMissingVaultAssignments === 0, + canaryEvidenceComplete: summary.requiredRowsMissingCanaryEvidence === 0, + productionStatusesComplete: summary.productionRows === summary.requiredForSpendRows, + }, + counts: summary, + blockers: readiness.blockers.map((row) => ({ + poolId: row.poolId, + chainId: row.chainId, + protocol: row.protocol, + status: row.status, + blockers: row.blockers, + })), + }; +} + +function mdTable(headers, rows) { + const header = `| ${headers.join(" | ")} |`; + const sep = `| ${headers.map((h) => (h.match(/count|rows|chain/i) ? "---:" : "---")).join(" | ")} |`; + const body = rows.map((row) => `| ${row.join(" | ")} |`); + return [header, sep, ...body].join("\n"); +} + +function escapeCell(value) { + return String(value ?? "-").replace(/\|/g, "\\|").replace(/\n/g, "
"); +} + +function writeReports(matrix, surface, summary, readiness, productionGate) { + mkdirSync(outDir, { recursive: true }); + + writeFileSync(resolve(outDir, "all-mainnet-deployment-readiness-worklist-latest.json"), `${JSON.stringify(readiness, null, 2)}\n`); + writeFileSync(resolve(outDir, "all-mainnet-production-gate-latest.json"), `${JSON.stringify(productionGate, null, 2)}\n`); + writeFileSync( + resolve(outDir, "all-mainnet-spend-readiness-latest.json"), + `${JSON.stringify( + { + generatedAt: summary.generatedAt, + version: matrix.version, + description: "Derived spend-readiness view for required ALL Mainnet pool rows.", + status: summary.productionReady ? "ready" : "blocked", + summary, + statusCounts: countBy(readiness.blockers, (row) => row.status), + routes: readiness.blockers.map((row) => ({ + routeId: row.poolId, + chainId: row.chainId, + network: row.network, + protocol: row.protocol, + status: row.blockers.includes("pool_not_created") + ? "missing_pool" + : row.blockers.includes("pool_created_but_not_funded") + ? "pool_created_unfunded" + : row.blockers.includes("missing_canary_evidence") + ? "missing_canary" + : "blocked", + path: `${row.baseToken?.symbol || "?"} -> ${row.quoteToken?.symbol || "?"}`, + blockers: row.blockers, + })), + assumptions: [ + "Spend readiness is blocked until every required pool has vault assignments, funding/reserve evidence, canary evidence, and production status.", + "ALL Mainnet same-chain routing remains blocked while config/allmainnet-non-dodo-protocol-surface.json has sameChainSwapInventoryPublished=false.", + ], + }, + null, + 2, + )}\n`, + ); + + const poolMatrixReport = { + generatedAt: summary.generatedAt, + summary, + requiredPools: matrix.rows.filter((row) => row.requiredForSpend === true).map(rowRef), + rowsByStatus: countBy(matrix.rows, (row) => row.status), + rowsByProtocol: countBy(matrix.rows, (row) => row.protocol), + }; + writeFileSync(resolve(outDir, "all-mainnet-pool-creation-matrix-latest.json"), `${JSON.stringify(poolMatrixReport, null, 2)}\n`); + + const summaryRows = [ + ["totalRows", summary.totalRows], + ["requiredForSpendRows", summary.requiredForSpendRows], + ["plannedRows", summary.plannedRows], + ["createdUnfundedRows", summary.createdUnfundedRows], + ["fundedRows", summary.fundedRows], + ["liveReadRows", summary.liveReadRows], + ["productionRows", summary.productionRows], + ["rowsMissingVaultAssignments", summary.rowsMissingVaultAssignments], + ["requiredRowsMissingVaultAssignments", summary.requiredRowsMissingVaultAssignments], + ["requiredRowsMissingCanaryEvidence", summary.requiredRowsMissingCanaryEvidence], + ["sameChainSwapInventoryPublished", summary.sameChainSwapInventoryPublished], + ["productionReady", summary.productionReady], + ]; + + const blockersRows = readiness.blockers.map((row) => [ + row.chainId, + `\`${escapeCell(row.poolId)}\``, + escapeCell(row.protocol), + escapeCell(row.status), + escapeCell(tokenRef(row.baseToken)), + escapeCell(tokenRef(row.quoteToken)), + escapeCell(row.poolAddress || "-"), + escapeCell(row.blockers.join(", ")), + ]); + + const readinessMd = [ + "# ALL Mainnet Deployment Readiness Worklist", + "", + `Generated: \`${summary.generatedAt}\``, + "", + "## Summary", + "", + mdTable(["Item", "Count"], summaryRows), + "", + "## Execution Order", + "", + ...readiness.executionOrder.map((step, index) => `${index + 1}. \`${step}\``), + "", + "## Required Pool Blockers", + "", + blockersRows.length + ? mdTable(["Chain", "Pool", "Protocol", "Status", "Base", "Quote", "Pool Address", "Blockers"], blockersRows) + : "No required pool blockers remain.", + "", + "## Protocol Surface Blockers", + "", + readiness.protocolSurfaceBlockers.length + ? mdTable( + ["Protocol", "Family", "Status", "Factory", "Router", "Blockers"], + readiness.protocolSurfaceBlockers.map((protocol) => [ + escapeCell(protocol.name), + escapeCell(protocol.family), + escapeCell(protocol.status), + escapeCell(protocol.factoryAddress || "-"), + escapeCell(protocol.routerAddress || "-"), + escapeCell(protocol.blockers.join(", ")), + ]), + ) + : "No protocol surface blockers remain.", + "", + ].join("\n"); + writeFileSync(resolve(outDir, "all-mainnet-deployment-readiness-worklist-latest.md"), readinessMd); + + const spendRows = readiness.blockers.map((row) => [ + `\`${escapeCell(row.poolId)}\``, + row.chainId, + escapeCell(row.protocol), + escapeCell(`${row.baseToken?.symbol || "?"} -> ${row.quoteToken?.symbol || "?"}`), + escapeCell( + row.blockers.includes("pool_not_created") + ? "missing_pool" + : row.blockers.includes("pool_created_but_not_funded") + ? "pool_created_unfunded" + : row.blockers.includes("missing_canary_evidence") + ? "missing_canary" + : "blocked", + ), + escapeCell(row.blockers.join(", ")), + ]); + const spendMd = [ + "# ALL Mainnet Spend Readiness", + "", + `Generated: \`${summary.generatedAt}\``, + "", + "## Summary", + "", + mdTable(["Item", "Count"], summaryRows), + "", + "## Required Route Gates", + "", + spendRows.length + ? mdTable(["Route", "Chain", "Protocol", "Path", "Status", "Blockers"], spendRows) + : "All required spend routes are production-ready.", + "", + "## Direct Mapping Policy", + "", + "Direct ALL Mainnet -> public-network mappings remain inventory-only until verified bridge adapters, fee paths, vault assignments, live reserve reads, and canary evidence are recorded.", + "", + ].join("\n"); + writeFileSync(resolve(outDir, "all-mainnet-spend-readiness-latest.md"), spendMd); + + const requiredPoolRows = poolMatrixReport.requiredPools.map((row) => [ + row.chainId, + `\`${escapeCell(row.poolId)}\``, + escapeCell(row.protocol), + escapeCell(row.status), + escapeCell(row.poolAddress || "-"), + escapeCell((row.missingRequiredVaultRoles || []).join(", ") || "-"), + ]); + const poolMatrixMd = [ + "# ALL Mainnet Pool Creation Matrix", + "", + `Generated: \`${summary.generatedAt}\``, + "", + "## Summary", + "", + mdTable(["Status", "Count"], Object.entries(summary.statusCounts)), + "", + "## Required Pools", + "", + mdTable(["Chain", "Pool", "Protocol", "Status", "Address", "Missing Vault Roles"], requiredPoolRows), + "", + ].join("\n"); + writeFileSync(resolve(outDir, "all-mainnet-pool-creation-matrix-latest.md"), poolMatrixMd); +} + +const matrix = readJson(matrixPath); +const surface = readJson(surfacePath); +const summary = makeSummary(matrix, surface); +const readiness = makeReadiness(matrix, surface, summary); +const productionGate = makeProductionGate(summary, readiness, surface); + +writeReports(matrix, surface, summary, readiness, productionGate); + +console.log(`[OK] ALL Mainnet readiness reports generated (${readiness.status}).`); diff --git a/scripts/verify/check-allmainnet-protocol-surface.sh b/scripts/verify/check-allmainnet-protocol-surface.sh index ab03fcc7..6da3e213 100644 --- a/scripts/verify/check-allmainnet-protocol-surface.sh +++ b/scripts/verify/check-allmainnet-protocol-surface.sh @@ -22,8 +22,38 @@ jq -e ' and (.summary | type == "object") and (.summary.bridgeOnlyLive | type == "boolean") and (.summary.sameChainSwapInventoryPublished | type == "boolean") + and (.classificationFramework.metadataDomains | type == "array") + and ((.classificationFramework.metadataDomains - [ + "backingMetadata", + "bridgeMetadata", + "cashMetadata", + "commodityMetadata", + "reserveMetadata", + "securityMetadata", + "settlementMetadata" + ]) | length == 0) + and (.documentedTokens | type == "array") + and ((.documentedTokens | length) > 0) + and all(.documentedTokens[]; . as $token | ( + (.symbol | type == "string") + and (.address | test("^0x[0-9a-fA-F]{40}$")) + and (.category | type == "string") + and (.instrumentType | type == "string") + and (.backingAssets | type == "array") + and (.tags | type == "array") + and (.backingMetadata | type == "object") + and (.bridgeMetadata | type == "object") + and (.cashMetadata | type == "object") + and (.commodityMetadata | type == "object") + and (.reserveMetadata | type == "object") + and (.securityMetadata | type == "object") + and (.settlementMetadata | type == "object") + and (($token.gruVersion == null) or (($token.tags | index("gru:" + $token.gruVersion)) != null)) + )) + and (.bridgeSurface.adapter.address | test("^0x[0-9a-fA-F]{40}$")) + and (.bridgeSurface.adapter.status == "live") ' "$F" >/dev/null || { - echo "[ERROR] allmainnet-non-dodo-protocol-surface.json: expected chainId 651940, status string, summary.bridgeOnlyLive and summary.sameChainSwapInventoryPublished booleans" + echo "[ERROR] allmainnet-non-dodo-protocol-surface.json: expected chainId 651940, summary flags, metadata domains, documented token metadata, GRU version tags, and live bridge adapter" exit 1 }