diff --git a/docs/pl-genesis-demo-script.md b/docs/pl-genesis-demo-script.md index bcefc145..b827d882 100644 --- a/docs/pl-genesis-demo-script.md +++ b/docs/pl-genesis-demo-script.md @@ -1,112 +1,168 @@ # PL Genesis Hackathon — Demo Video Script -**Target length**: 2–3 minutes -**Tone**: Wishonia narrating (deadpan, data-first, dry) +**Target length**: 3–4 minutes +**Tone**: Wishonia narrating (deadpan, data-first, dry, alien observer) --- -## 1. Hook (15s) +## 1. Hook — Your Government Is a Misaligned Superintelligence (20s) -> "Your governments are misaligned superintelligences — collective intelligence systems controlling billions of lives, optimising for re-election instead of welfare. I built alignment software. It works on my planet. Let's see if your species can handle it." +> "Your government is a misaligned superintelligence. It controls trillions of dollars, billions of lives, and the allocation of your civilisation's entire productive capacity. And it is optimising for the wrong objective function." -*Visual*: Optomitron homepage hero, the tagline scrolling in. +*Visual*: Optomitron homepage hero. The tagline animates in. + +> "The objective of the Earth Optimisation Game is to align it — to force it to reallocate resources away from things that make you poorer and deader, toward things that make you healthier and wealthier." --- -## 2. Wishocracy — Citizen Preferences (25s) +## 2. The Numbers (25s) + +> "Here are some numbers." -> "Step one: actually ask people what they want. Radical concept, I know." +*Visual*: Navigate to `/scoreboard`. Show the headline metrics — healthy lifespan, median income, collapse countdown. -*Visual*: Open `/wishocracy`. Show pairwise comparison cards — "Healthcare vs Defense", slider interaction. +> "150,000 humans die every day from diseases that are theoretically curable. That's 59 September 11ths. But nobody invades anybody about it because cancer doesn't have oil." -> "Citizens compare budget priorities head-to-head. Eigenvector decomposition produces stable preference weights from as few as ten comparisons. Your species invented this maths in 1977 and then mostly used it to rank sports teams." +> "Your governments currently spend $604 on the capacity for mass murder for every $1 they spend testing which medicines work. Your chance of dying from terrorism: 1 in 30 million. Your chance of dying from disease: 100%." -*Visual*: Show completion state with summary. Highlight the **"Content-addressed on IPFS"** badge with the Storacha CID. +*Visual*: Show the GDP collapse countdown timer. -> "Every preference snapshot is stored on Storacha — content-addressed, immutable, linked to its predecessor. No server can alter it after the fact." +> "The parasitic economy — cybercrime, rent-seeking, military spending — grows at 15% per year. The productive economy grows at 3%. In 15 years, it becomes more rational to steal than to produce. This is the clock." --- -## 3. Alignment — Politician Report Cards (20s) +## 3. The Moronia Problem (20s) -> "Step two: find out which of your elected officials actually agrees with you." +> "I've seen this before." -*Visual*: Navigate to `/alignment`. Show ranked politicians with scores. +*Visual*: Navigate to `/moronia`. Show the AI-generated images of Moronia. -> "Voting records are compared against citizen preferences. Each politician gets a Citizen Alignment Score — a single number that answers 'how much do they actually represent you?'" +> "Moronia was a planet that spent 604 times more on weapons than on curing disease. It no longer exists. Their allocation ratio correlates with yours at 94.7%." -*Visual*: Zoom into a politician card. Highlight the **"Verified on IPFS"** green badge. Click it to show the CID gateway link. +*Visual*: Hard cut to `/wishonia`. Show Wishonia imagery. + +> "Wishonia redirected 1% of its murder budget to clinical trials 4,297 years ago. That is where I am from. It is considerably nicer." --- -## 4. Hypercerts — Verifiable Attestations (25s) +## 4. The 1% Treaty — One Slide, Whole Solution (25s) + +> "The fix is not complicated." + +*Visual*: Navigate to the Wishocracy voting page or the game landing page. + +> "Redirect 1% of global military spending — $27 billion a year — to clinical trials. That's going from spending 99% on bombs to 98% on bombs. Radical, I know." + +> "This would increase clinical trial capacity by 12.3 times. It would compress the time to cure all diseases from 443 years to 36 years. The math is not in dispute." + +> "The problem is not that nobody wants this. The problem is that everybody wants it but thinks nobody else will agree to it. This is called pluralistic ignorance, and it is your civilisation's most expensive bug." + +--- + +## 5. The Game — Play It (30s) + +> "Step one: allocate." + +*Visual*: Navigate to `/wishocracy`. Show the pairwise comparison interface. + +> "Indicate your preferred share of spending devoted to weapons versus clinical trials. Compare budget priorities head-to-head. Ten comparisons. Eigenvector decomposition. The same maths invented in 1977 that your species mostly uses to rank American football teams." + +*Visual*: Complete a few comparisons. Show the results summary. -> "Every alignment score is published as a Hypercert on the AT Protocol and stored on Storacha." +> "Step two: vote yes or no — should all governments redirect 1% of military spending to clinical trials?" -*Visual*: Navigate to `/transparency`. Show the attestation records grid — IPFS CIDs, AT Protocol URIs. +> "Step three: get your shareable URL." -> "Click any CID. You'll see the raw JSON — the Activity claim, the Measurements, the Evaluation. Tamper-proof. Auditable by anyone. This is what accountability looks like when you take it seriously." +*Visual*: Show a shareable link generating. -*Visual*: Click a Storacha gateway link, show raw JSON in browser. +> "Every friend who votes through your link earns you alignment points. Your network becomes your lobby. Decentralised. No headquarters. No PAC. Just maths and peer pressure." --- -## 5. Earth Optimization Prize (20s) +## 6. The Prize Pool — The Investment Nobody Loses (25s) -> "Traditional philanthropy: give money, hope it works, never check. My approach: outcome-based escrow." +> "Step four: get your rich friends to invest in the Earth Optimisation Prize Pool." -*Visual*: Navigate to `/prize`. Show the mechanism steps and donation section. +*Visual*: Navigate to `/prize`. Show the prize pool mechanism. -> "Donors deposit $WISH into a Prize Pool smart contract. The funds are locked until health and income metrics cross verifiable thresholds. Then donors vote on which implementers get paid — weighted by deposit amount. Your deposit IS your identity. No sybil attacks possible." +> "The Prize Pool is a Dominant Assurance Contract diversified across venture capital, achieving 17% annual returns. Here is how it works:" -*Visual*: Highlight the PrizePool contract details — deposit-as-identity, Storacha evidence CIDs, bonded disputes. +> "Donors deposit $WISH into a smart contract. If Earth hits its targets — median income up, healthy life years up — the funds unlock and flow to verified implementers. If Earth fails to hit its targets, depositors get their principal back. Plus 17% annual gains. The gains that were achievable anyway by diversifying across venture capital." -> "Implementers register with Storacha CIDs linking to their Hypercert evidence. Challengers can post a bond to dispute allocations. Lose → you forfeit the bond. Win → the fraudster gets deactivated. Accountability with teeth." +*Visual*: Highlight the smart contract logic — escrow, thresholds, refund conditions. + +> "This is called a free option. You either get a healthier, wealthier planet, or you get your money back with interest. The only way to lose is not to play." + +--- + +## 7. Incentive Alignment Bonds (20s) + +> "Now for the part your lobbyists really won't enjoy." + +*Visual*: Show the $WISH token / IAB section. + +> "Incentive Alignment Bonds. Sell one billion dollars of these. Use the proceeds to fund a 1% Treaty campaign. Create a Special Purpose Vehicle that receives treaty inflows — 1% of military spending per year, $27 billion annually. Distribute: 10% to bond holders, 10% to a Super PAC, 80% to fund clinical trials." + +> "The Super PAC funds politicians algorithmically — based on their Citizen Alignment Score. Politicians earn campaign funding by voting for the treaty and increasing the reallocation. Not by attending donor dinners." --- -## 6. $WISH Token + IAB (25s) +## 8. Politician Scores — The Leaderboard They Fear (15s) + +> "Which brings us to the leaderboard." + +*Visual*: Navigate to `/governments/US/politicians`. Show politician alignment rankings. + +> "Every politician ranked by the ratio of spending they have voted for: mass murder capacity versus clinical trial funding. A single number. Public. Immutable. On-chain." + +*Visual*: Scroll through rankings. Highlight the alignment score column. + +> "Your leaders aren't evil. They're just optimising for the wrong metric. We changed the metric." + +--- -> "Now for the part your campaign finance lobbyists won't enjoy." +## 9. Optimal Policy Tools (15s) -*Visual*: Show the $WISH Token & UBI section on the transparency page. +> "For the politicians who want to align:" -> "The $WISH token has a 0.5% transaction tax. That replaces your IRS. No 74,000-page tax code. No 83,000 employees. Revenue collection as a protocol feature." +*Visual*: Navigate to `/tools` or the Optomitron budget/policy pages. -> "The tax funds two things: Universal Basic Income — distributed automatically to verified citizens via World ID — and Incentive Alignment Bonds, where smart contracts distribute campaign funds to politicians based on their Hypercert-verified alignment scores." +> "The Optomitron contains an Optimal Budget Generator and Optimal Policy Generator. It analyses time-series data across hundreds of jurisdictions — which policies actually increased median income and healthy life years. Not which policies were popular. Which ones worked." -*Visual*: Show the pipeline diagram. Highlight each step. +*Visual*: Show the budget optimiser, analysis explorer. -> "Politicians earn funding by aligning with citizens, not donors. No PACs. No lobbyists. Just maths." +> "All free. All open. All citable." --- -## 7. Architecture (15s) +## 10. Architecture (15s) -> "Under the hood: 15 packages, 2,600+ tests, a domain-agnostic causal inference engine, and a fully typed TypeScript monorepo." +> "Under the hood: 15 packages, 2,600+ tests, domain-agnostic causal inference, full TypeScript monorepo. Storacha for immutable content-addressed storage. Hypercerts for verifiable attestations. Solidity for enforceable incentives." -*Visual*: Quick scroll through the GitHub repo. Show package structure. Flash test output. +*Visual*: Quick GitHub repo scroll. Package structure. Test output. -> "Storacha for content-addressed storage. Hypercerts for verifiable attestations. Solidity for enforceable incentives. All open source." +> "Everything is auditable. Nothing relies on trusting us." --- -## 8. Close (15s) +## 11. Close (15s) -> "Storacha makes governance data immutable. Hypercerts make it auditable. Smart contracts make it enforceable. Your species has had these tools for years. You just keep not using them." +> "Your governments are the most powerful artificial intelligences your species has ever built. They process more information, control more resources, and make more consequential decisions than any LLM. And they are misaligned." -*Visual*: Return to homepage. Show the tagline. +*Visual*: Return to homepage. > "Optomitron. Alignment software for the most powerful AIs on your planet — the ones made of people." -*Visual*: GitHub URL. End card. +*Visual*: GitHub URL. Warondisease.org. End card. --- ## Technical Notes - Record at 1080p minimum -- Use browser dev tools to hide any loading states - Pre-populate Wishocracy with enough comparisons to show a complete report -- Have the Storacha gateway CID link pre-loaded in a separate tab for quick cut -- Background music: something understated and slightly ominous (think BBC documentary) +- Pre-load Storacha gateway CID in a separate tab for quick cut +- Have the Moronia → Wishonia cut timed to land as a punchline +- Collapse countdown should be visible and ticking in real-time +- Background music: understated, slightly ominous (BBC documentary energy) +- Capture politician leaderboard with real data visible — the numbers are the joke diff --git a/packages/treasury/IAB-GAP-ANALYSIS.md b/packages/treasury/IAB-GAP-ANALYSIS.md new file mode 100644 index 00000000..83d0788d --- /dev/null +++ b/packages/treasury/IAB-GAP-ANALYSIS.md @@ -0,0 +1,93 @@ +# IAB Implementation Gap Analysis + +## What Was Already Here + +| Contract | Status | Implements | +|---|---|---| +| `WishToken.sol` | ✅ Complete | ERC-20 with transaction tax → AlignmentTreasury | +| `AlignmentTreasury.sol` | ✅ Complete | Layer 2 (politician alignment scores + UBI) | +| `PrizePool.sol` | ⚠️ Partial | Prize escrow, wishocratic allocation, disputes | + +## What Was Missing + +### 1. `VictoryBond.sol` — THE CORE IAB INSTRUMENT ❌ → ✅ Now added + +The entire investor-facing instrument was absent. The IAB paper describes bondholders +who buy bonds, earn returns from treaty revenue, and therefore lobby for adoption. +Without this contract, the IAB mechanism has no financial instrument for investors. + +**Added in this PR:** +- `buyBond()` — investors deposit USDC, earn idle yield (T-bills) while waiting +- `receiveRevenue()` — treaty revenue flows in, splits 60/30/10 (holders/alignment/layer3) +- `claimRevenueShare()` — pro-rata revenue share for bondholders +- `claimIdleYield()` — T-bill-equivalent yield while funds sit idle +- `claimAssuranceRefund()` — full principal + yield if threshold never met by deadline +- `activateGlobalFailureRefund()` — oracle-callable if zero gains at T+15 +- `claimGlobalFailureRefund()` — principal + 15 years yield if global failure clause fires +- `disburseLayer3Reserve()` — post-office career program funding (off-chain disbursement) + +### 2. `PrizePool.sol` — Three gaps vs. the paper + +#### Gap A: Binary threshold trigger vs. proportional release ⚠️ +**Current:** Both health AND income metrics must hit thresholds simultaneously → pool unlocks all at once. +**Paper says:** Treasury releases *proportionally* as terminal metrics improve. Each basis point of improvement in each metric unlocks a proportional fraction of the pool. +**Fix needed:** Replace binary `updateMetrics()` unlock with proportional release curve. E.g., health pool releases linearly as `currentHealthMetric / healthMaxMetric`, income pool similarly. + +#### Gap B: No yield-bearing escrow ❌ +**Current:** WISH tokens sit inert in the contract. +**Paper says:** All contributions earn yield (T-bills/ERC-4626) while held. +**Fix needed:** Integrate an ERC-4626 yield vault. On deposit, push to vault. On distribution/refund, withdraw from vault (principal + yield). + +#### Gap C: No 15-year global failure refund ❌ +**Current:** No refund mechanism after threshold is met. +**Paper says:** If Optimitron verifies zero cumulative gains in both terminal metrics at T+15, full principal + accrued yield returned. +**Fix needed:** Add `deploymentTimestamp`, `verifiedGainsRecorded` flag, `activateGlobalFailureRefund()`, `claimGlobalFailureRefund()`. Same pattern as VictoryBond above. + +#### Gap D: One-time distribute() vs. outcome perpetuity ⚠️ +**Current:** `distribute()` is a one-time function that sets status to `Distributed`. +**Paper says:** Outcome perpetuity — implementers earn a *continuing* revenue share as long as they keep producing results. One-time payout removes the incentive to continue. +**Fix needed:** Replace one-shot distribute with a recurring claim model: each verified metric update releases a proportional tranche; implementers claim their share via `claimPerpetuityShard()` that can be called repeatedly as new tranches are released. + +### 3. AlignmentTreasury — One gap + +#### Gap: No connection to VictoryBond revenue intake ❌ +**Current:** Treasury receives $WISH from transaction tax and distributes to politicians. There's no path for treaty-generated revenue to flow into VictoryBond. +**Fix needed:** Add `sendTreatyRevenueToVictoryBond(uint256 amount)` function on AlignmentTreasury that calls `VictoryBond.receiveRevenue()`. This closes the loop: treaty success → $WISH flows into AlignmentTreasury → portion forwarded to VictoryBond → bondholder returns rise → more lobbying. + +## The Self-Completing Loop (from prize paper) + +With all gaps closed, the on-chain loop looks like this: + +``` +PrizePool.updateMetrics() // Optimitron reports dHealthy_med + gIncome_med gains + → proportional treasury release // funds flow to verified implementers + → AlignmentTreasury.receiveTreatyRevenue() // portion of implementation revenue + → VictoryBond.receiveRevenue() // 30% forwarded to alignment + 60% to bondholders + → bondholder returns rise // $WISH earnings increase for existing bondholders + → more investors buy VictoryBond // rational selfish response to higher returns + → more lobbying for treaty // bondholder self-interest drives political pressure + → more treaty adoption // more jurisdictions sign + → more metric gains // PrizePool.updateMetrics() called again + → cycle repeats, self-amplifying +``` + +No actor in this loop needs to be altruistic. Each step is the utility-maximizing +choice for the actor at that step. + +## Priority Order for Implementation + +1. **VictoryBond.sol** — ✅ Done in this PR (core IAB instrument) +2. **PrizePool: 15-year refund + yield vault** — Gaps C+B (closes "no lose" property for prize contributors) +3. **AlignmentTreasury → VictoryBond connection** — Gap 3 (closes the self-completing loop on-chain) +4. **PrizePool: proportional release** — Gap A (closer to paper spec; lower priority than refund) +5. **PrizePool: outcome perpetuity** — Gap D (important for long-term incentives; can ship after v1) + +## What Does NOT Need To Be On-Chain + +- **Layer 3 post-office careers** — matching politicians to consulting contracts is + inherently off-chain. VictoryBond reserves the 10% allocation; disbursement is + governance-controlled via the `disburseLayer3Reserve()` owner function. +- **Wishocratic pairwise comparison** — computation happens off-chain; only the + aggregated allocation weights are posted on-chain. Already correctly implemented. +- **Optimitron causal inference** — the oracle itself is off-chain; only its + signed output is submitted to `PrizePool.updateMetrics()`. diff --git a/packages/treasury/contracts/VictoryBond.sol b/packages/treasury/contracts/VictoryBond.sol new file mode 100644 index 00000000..9d21e3a9 --- /dev/null +++ b/packages/treasury/contracts/VictoryBond.sol @@ -0,0 +1,412 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; + +/** + * @title VictoryBond — Incentive Alignment Bond (IAB) + * @notice Tokenized bond instrument that aligns investor self-interest with + * treaty adoption. Investors earn returns from the revenue share of + * treaty-generated funding flows, proportional to verified alignment + * gains in terminal metrics (dHealthy_med, gIncome_med). + * + * Three-layer mechanism (from IAB paper): + * Layer 1 — INVESTOR ALIGNMENT + * Investors buy bonds by depositing stablecoin (USDC). Returns are + * proportional to the AlignmentTreasury revenue share received. When + * treaty metrics improve, more revenue flows in, returns rise, more + * investors buy bonds, more lobbying happens. The flywheel. + * + * Layer 2 — POLITICIAN ALIGNMENT + * AlignmentTreasury distributes $WISH to politicians proportional to + * their alignment scores (handled in AlignmentTreasury.sol). VictoryBond + * feeds a share of its revenue into the AlignmentTreasury to fund this. + * + * Layer 3 — POST-OFFICE CAREER GUARANTEE + * Off-chain: high-scoring politicians are matched to consulting contracts, + * board seats, and speaking opportunities funded from the perpetuity stream. + * Cannot be implemented on-chain; modeled here as a reserved allocation. + * + * Dominant assurance structure: + * - If the funding threshold is not met by `assuranceDeadline`: + * investors get their full principal back (assurance refund) + * - If threshold is met but zero verified metric gains by T+15 years: + * investors get principal + all accrued yield (global failure refund) + * - If gains are verified: revenue share flows perpetually + * + * Revenue flow: + * 1. AlignmentTreasury.receiveTreatyRevenue() → VictoryBond.receiveRevenue() + * 2. VictoryBond splits revenue: 60% to bondholders, 30% to AlignmentTreasury + * (for politician alignment), 10% reserved for Layer 3 post-office careers + * 3. Bondholders claim their pro-rata share of accumulated revenue + * + * Self-completing loop (prize paper, "The Self-Completing Loop"): + * VictoryBond ↔ AlignmentTreasury ↔ PrizePool + * - Metric gains in PrizePool → revenue into AlignmentTreasury → + * revenue share into VictoryBond → higher returns → more bond buyers → + * more lobbying → more metric gains → more treasury releases + */ +contract VictoryBond is Ownable, ReentrancyGuard { + using SafeERC20 for IERC20; + + // --- Token references --- + IERC20 public depositToken; // USDC or equivalent stablecoin + address public alignmentTreasury; // AlignmentTreasury contract + + // --- Funding threshold / assurance --- + uint256 public fundingThreshold; // Minimum deposits to activate + uint256 public assuranceDeadline; // Unix timestamp: refund if threshold not met by this date + bool public thresholdMet; // Latches true when threshold crossed + + // --- 15-year global failure refund --- + uint256 public deploymentTimestamp; // T0: timestamp when threshold was first met + uint256 public constant REFUND_WINDOW = 15 * 365 days; + bool public verifiedGainsRecorded; // Latches true when any gain triggers a payout + bool public globalFailureRefundActive; // True after zero-gain snapshot at T+15 + + // --- Bondholder tracking --- + struct BondPosition { + uint256 principal; + uint256 accruedYield; // Safe yield earned while funds sit idle + uint256 revenueShareDebt; // Revenue accumulated before this deposit (for fair accounting) + bool exists; + } + + mapping(address => BondPosition) public positions; + address[] public bondholderList; + uint256 public totalPrincipal; + + // --- Yield-bearing idle funds --- + // When threshold not yet met (or treasury idle), principal sits in safe yield + // Implemented as accumulated yield tracked off-chain; simplified here as rate + timestamp + uint256 public idleYieldRateBps; // Annual yield on idle funds (basis points, e.g. 500 = 5%) + uint256 public lastYieldTimestamp; + + // --- Revenue share tracking --- + // Revenue from treaty flows; distributed proportionally to bondholders + uint256 public totalRevenuReceived; + uint256 public totalRevenueDistributed; + uint256 public revenuePerBondUnit; // Cumulative revenue per 1e18 units of principal (scaled) + uint256 private constant REVENUE_SCALE = 1e18; + + // --- Revenue allocation splits (basis points) --- + uint256 public constant BONDHOLDER_SPLIT_BPS = 6000; // 60% to bondholders + uint256 public constant ALIGNMENT_SPLIT_BPS = 3000; // 30% to AlignmentTreasury (politician alignment) + uint256 public constant LAYER3_SPLIT_BPS = 1000; // 10% reserved for Layer 3 post-office careers + + // Layer 3 reserve (custodied here; disbursed off-chain via owner) + uint256 public layer3Reserve; + + // --- Events --- + event BondPurchased(address indexed buyer, uint256 amount); + event AssuranceRefundClaimed(address indexed holder, uint256 amount); + event GlobalFailureRefundActivated(uint256 timestamp); + event GlobalFailureRefundClaimed(address indexed holder, uint256 principal, uint256 yield); + event RevenueReceived(uint256 amount); + event RevenueShareClaimed(address indexed holder, uint256 amount); + event YieldClaimed(address indexed holder, uint256 amount); + event ThresholdMet(uint256 totalPrincipal, uint256 timestamp); + event VerifiedGainRecorded(uint256 timestamp); + event Layer3Reserved(uint256 amount); + + constructor( + address _depositToken, + address _alignmentTreasury, + uint256 _fundingThreshold, + uint256 _assuranceDeadlineDays, + uint256 _idleYieldRateBps + ) Ownable(msg.sender) { + require(_depositToken != address(0), "VictoryBond: zero token"); + require(_alignmentTreasury != address(0), "VictoryBond: zero treasury"); + require(_fundingThreshold > 0, "VictoryBond: zero threshold"); + require(_idleYieldRateBps <= 2000, "VictoryBond: yield too high"); // max 20% + + depositToken = IERC20(_depositToken); + alignmentTreasury = _alignmentTreasury; + fundingThreshold = _fundingThreshold; + assuranceDeadline = block.timestamp + (_assuranceDeadlineDays * 1 days); + idleYieldRateBps = _idleYieldRateBps; + lastYieldTimestamp = block.timestamp; + } + + // ========================================================================= + // INVESTOR FUNCTIONS + // ========================================================================= + + /** + * @notice Buy bonds by depositing stablecoin. Principal earns idle yield + * until the funding threshold is met, then transitions to revenue share. + */ + function buyBond(uint256 amount) external nonReentrant { + require(amount > 0, "VictoryBond: zero amount"); + require(!globalFailureRefundActive, "VictoryBond: global failure refund active"); + + // Accrue idle yield for all existing holders before new deposit changes the base + _accrueIdleYield(); + + depositToken.safeTransferFrom(msg.sender, address(this), amount); + + if (!positions[msg.sender].exists) { + bondholderList.push(msg.sender); + positions[msg.sender].exists = true; + } + + // Set revenue debt to current accumulated revenue (fair: new money doesn't claim old revenue) + positions[msg.sender].revenueShareDebt += (revenuePerBondUnit * amount) / REVENUE_SCALE; + positions[msg.sender].principal += amount; + totalPrincipal += amount; + + // Check if threshold just crossed + if (!thresholdMet && totalPrincipal >= fundingThreshold) { + thresholdMet = true; + deploymentTimestamp = block.timestamp; + emit ThresholdMet(totalPrincipal, block.timestamp); + } + + emit BondPurchased(msg.sender, amount); + } + + /** + * @notice Claim accumulated idle yield (earned on principal while threshold not yet met, + * or during periods of low treaty revenue). + */ + function claimIdleYield() external nonReentrant { + _accrueIdleYield(); + uint256 yield = positions[msg.sender].accruedYield; + require(yield > 0, "VictoryBond: no yield"); + + positions[msg.sender].accruedYield = 0; + depositToken.safeTransfer(msg.sender, yield); + emit YieldClaimed(msg.sender, yield); + } + + /** + * @notice Claim revenue share from treaty-generated funding flows. + * This is the primary return mechanism once the treaty is adopted. + */ + function claimRevenueShare() external nonReentrant { + uint256 share = _pendingRevenueShare(msg.sender); + require(share > 0, "VictoryBond: no revenue share"); + + positions[msg.sender].revenueShareDebt = + (revenuePerBondUnit * positions[msg.sender].principal) / REVENUE_SCALE; + + depositToken.safeTransfer(msg.sender, share); + totalRevenueDistributed += share; + emit RevenueShareClaimed(msg.sender, share); + } + + // ========================================================================= + // ASSURANCE REFUND (pre-threshold failure) + // ========================================================================= + + /** + * @notice Claim full principal refund if threshold was never met by the deadline. + * Principal + all accrued idle yield returned. + */ + function claimAssuranceRefund() external nonReentrant { + require(!thresholdMet, "VictoryBond: threshold was met"); + require(block.timestamp > assuranceDeadline, "VictoryBond: deadline not passed"); + + _accrueIdleYield(); + + uint256 principal = positions[msg.sender].principal; + uint256 yield = positions[msg.sender].accruedYield; + require(principal > 0, "VictoryBond: no position"); + + positions[msg.sender].principal = 0; + positions[msg.sender].accruedYield = 0; + totalPrincipal -= principal; + + depositToken.safeTransfer(msg.sender, principal + yield); + emit AssuranceRefundClaimed(msg.sender, principal + yield); + } + + // ========================================================================= + // 15-YEAR GLOBAL FAILURE REFUND (post-threshold, no verified gains) + // ========================================================================= + + /** + * @notice Activate the global failure refund clause. + * Called by owner/oracle after Optimitron snapshot confirms zero + * cumulative gains in dHealthy_med and gIncome_med across all + * adopting jurisdictions at T+15 years. + * + * Requires: + * - threshold was met (deployment happened) + * - 15 years have passed since deployment + * - no verified gains have ever been recorded + */ + function activateGlobalFailureRefund() external onlyOwner { + require(thresholdMet, "VictoryBond: threshold not met"); + require( + block.timestamp >= deploymentTimestamp + REFUND_WINDOW, + "VictoryBond: 15 years not elapsed" + ); + require(!verifiedGainsRecorded, "VictoryBond: gains already verified (refund expired)"); + require(!globalFailureRefundActive, "VictoryBond: already activated"); + + globalFailureRefundActive = true; + emit GlobalFailureRefundActivated(block.timestamp); + } + + /** + * @notice Claim full principal + all accrued yield after global failure refund activates. + */ + function claimGlobalFailureRefund() external nonReentrant { + require(globalFailureRefundActive, "VictoryBond: refund not active"); + + _accrueIdleYield(); + + uint256 principal = positions[msg.sender].principal; + uint256 yield = positions[msg.sender].accruedYield; + require(principal > 0, "VictoryBond: no position"); + + positions[msg.sender].principal = 0; + positions[msg.sender].accruedYield = 0; + totalPrincipal -= principal; + + depositToken.safeTransfer(msg.sender, principal + yield); + emit GlobalFailureRefundClaimed(msg.sender, principal, yield); + } + + // ========================================================================= + // REVENUE INTAKE (from treaty funding flows) + // ========================================================================= + + /** + * @notice Receive treaty revenue and split it across bondholders, + * AlignmentTreasury (politician alignment), and Layer 3 reserve. + * + * Called by AlignmentTreasury when treaty-generated funding flows in. + * This is the key coupling: treaty success → revenue → bondholder returns. + */ + function receiveRevenue(uint256 amount) external nonReentrant { + require(amount > 0, "VictoryBond: zero revenue"); + require(thresholdMet, "VictoryBond: threshold not met"); + + depositToken.safeTransferFrom(msg.sender, address(this), amount); + + // Mark that verified gains exist — defeats global failure refund clause + if (!verifiedGainsRecorded) { + verifiedGainsRecorded = true; + emit VerifiedGainRecorded(block.timestamp); + } + + totalRevenuReceived += amount; + + uint256 bondholderShare = (amount * BONDHOLDER_SPLIT_BPS) / 10_000; + uint256 alignmentShare = (amount * ALIGNMENT_SPLIT_BPS) / 10_000; + uint256 layer3Share = (amount * LAYER3_SPLIT_BPS) / 10_000; + + // Update revenue-per-unit for pro-rata bondholder claims + if (totalPrincipal > 0) { + revenuePerBondUnit += (bondholderShare * REVENUE_SCALE) / totalPrincipal; + } + + // Forward alignment share to AlignmentTreasury immediately + if (alignmentShare > 0) { + depositToken.safeTransfer(alignmentTreasury, alignmentShare); + } + + // Accumulate Layer 3 reserve (disbursed off-chain by owner) + layer3Reserve += layer3Share; + + emit RevenueReceived(amount); + } + + /** + * @notice Disburse Layer 3 reserve to off-chain post-office career program. + * Only owner. In production, this is governed by a multisig + Wishocracy vote. + */ + function disburseLayer3Reserve(address recipient, uint256 amount) external onlyOwner { + require(amount <= layer3Reserve, "VictoryBond: exceeds reserve"); + layer3Reserve -= amount; + depositToken.safeTransfer(recipient, amount); + } + + // ========================================================================= + // INTERNAL YIELD ACCRUAL + // ========================================================================= + + /** + * @notice Accrue idle yield to all bondholders pro-rata since last accrual. + * Simplified linear model. In production: integrate with ERC-4626 vault. + * + * Idle yield accrues whenever principal is sitting uninvested — + * i.e., before treaty revenue starts flowing. Represents T-bill returns + * (or ERC-4626 vault returns) on the deposited principal. + */ + function _accrueIdleYield() internal { + if (bondholderList.length == 0 || totalPrincipal == 0) { + lastYieldTimestamp = block.timestamp; + return; + } + + uint256 elapsed = block.timestamp - lastYieldTimestamp; + if (elapsed == 0) return; + + // Annual rate: idleYieldRateBps / 10000 + // Per-second rate: idleYieldRateBps / (10000 * 365 days) + uint256 totalYield = (totalPrincipal * idleYieldRateBps * elapsed) / (10_000 * 365 days); + + if (totalYield == 0) { + lastYieldTimestamp = block.timestamp; + return; + } + + // Distribute proportionally to all holders + for (uint256 i = 0; i < bondholderList.length; i++) { + address holder = bondholderList[i]; + if (positions[holder].principal == 0) continue; + + uint256 share = (totalYield * positions[holder].principal) / totalPrincipal; + positions[holder].accruedYield += share; + } + + lastYieldTimestamp = block.timestamp; + } + + function _pendingRevenueShare(address holder) internal view returns (uint256) { + if (positions[holder].principal == 0) return 0; + uint256 entitled = (revenuePerBondUnit * positions[holder].principal) / REVENUE_SCALE; + uint256 debt = positions[holder].revenueShareDebt; + return entitled > debt ? entitled - debt : 0; + } + + // ========================================================================= + // VIEW FUNCTIONS + // ========================================================================= + + function bondholderCount() external view returns (uint256) { + return bondholderList.length; + } + + function pendingRevenueShare(address holder) external view returns (uint256) { + return _pendingRevenueShare(holder); + } + + function isAssuranceRefundEligible() external view returns (bool) { + return !thresholdMet && block.timestamp > assuranceDeadline; + } + + function isGlobalFailureRefundEligible() external view returns (bool) { + return thresholdMet && + block.timestamp >= deploymentTimestamp + REFUND_WINDOW && + !verifiedGainsRecorded; + } + + function timeUntilRefundWindow() external view returns (uint256) { + if (!thresholdMet) return 0; + uint256 target = deploymentTimestamp + REFUND_WINDOW; + if (block.timestamp >= target) return 0; + return target - block.timestamp; + } + + function contractBalance() external view returns (uint256) { + return depositToken.balanceOf(address(this)); + } +} diff --git a/packages/web/e2e/demo-recording.spec.ts b/packages/web/e2e/demo-recording.spec.ts index 5a40340f..34a17c4f 100644 --- a/packages/web/e2e/demo-recording.spec.ts +++ b/packages/web/e2e/demo-recording.spec.ts @@ -1,348 +1,409 @@ /** * Playwright E2E: Demo recording for PL Genesis Hackathon. * - * Records a 1920x1080 video walking through the site in demo-script order. - * Includes full Wishocracy pairwise comparison flow. + * Narrative: "Your government is a misaligned superintelligence. + * Here's the alignment software." + * + * Records a 1920x1080 video walking through the full narrative arc: + * 1. Hook — homepage hero + * 2. The numbers — Invisible Graveyard, $604 war vs $1 cures + * 3. The 1% Treaty — One Percent Treaty section + * 4. Wishocracy — pairwise allocation flow (full) + * 5. Alignment — politician report cards + * 6. Misconceptions — myth vs data + * 7. Compare — country outcomes + * 8. Transparency — Hypercerts / IPFS attestations + * 9. Prize Pool — dominant assurance contract + * 10. IAB — Incentive Alignment Bonds + * 11. Close — homepage hero * * Run: - * pnpm --filter @optomitron/web exec playwright test e2e/demo-recording.spec.ts + * BASE_URL=http://localhost:3001 pnpm --filter @optomitron/web exec playwright test e2e/demo-recording.spec.ts --project=demo-recording * * Output: packages/web/test-results/ (video files) */ -import { test, expect } from "@playwright/test"; -const SECTION_PAUSE = 3_000; // breathing room between sections +import { test, expect, type Page } from "@playwright/test"; + +// ─── Timing constants ──────────────────────────────────────────────────────── +const SECTION_PAUSE = 2_500; // breathing room between sections +const SCROLL_PAUSE = 1_800; // after a scroll +const BEAT_PAUSE = 1_200; // short beat inside a section + +// ─── Helpers ───────────────────────────────────────────────────────────────── async function pause(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } -/** Navigate and wait for CSS, fonts, and images to fully load */ -async function navigateTo( - page: import("@playwright/test").Page, - path: string, -) { +/** Navigate and wait for fonts + images to fully settle */ +async function go(page: Page, path: string, extraMs = 1500) { await page.goto(path, { waitUntil: "networkidle" }); await page.waitForLoadState("load"); await page.evaluate(() => document.fonts.ready); - await pause(1500); + await pause(extraMs); } -/** - * Set the range slider using Playwright's fill() which triggers React onChange. - */ -async function setSlider( - page: import("@playwright/test").Page, - value: number, -) { +/** Smooth-scroll to an absolute Y position */ +async function scrollTo(page: Page, y: number, waitMs = SCROLL_PAUSE) { + await page.evaluate((top) => window.scrollTo({ top, behavior: "smooth" }), y); + await pause(waitMs); +} + +/** Scroll until a text string is in view; safe (no-op if not found) */ +async function scrollToText(page: Page, text: string, waitMs = SCROLL_PAUSE) { + const el = page.locator(`text="${text}"`).first(); + if ((await el.count()) > 0) { + await el.scrollIntoViewIfNeeded(); + await pause(waitMs); + } +} + +/** Scroll until a locator is in view; safe no-op if not found */ +async function scrollToLocator(page: Page, locator: import("@playwright/test").Locator, waitMs = SCROLL_PAUSE) { + if ((await locator.count()) > 0) { + await locator.first().scrollIntoViewIfNeeded(); + await pause(waitMs); + } +} + +/** Set a range slider via fill() which triggers React onChange */ +async function setSlider(page: Page, value: number) { const slider = page.locator('input[type="range"]'); await slider.waitFor({ state: "visible" }); - - // Playwright's fill() on range inputs sets the value and fires all necessary events await slider.fill(String(value)); await pause(600); } -// How many categories to fund (more = more pairwise comparisons) -// 5 categories = 10 pairs, which is a good demo length -const CATEGORIES_TO_FUND = 5; -// Total budget categories in the system -const TOTAL_CATEGORIES = 17; +// ─── Demo constants ─────────────────────────────────────────────────────────── +const CATEGORIES_TO_FUND = 5; // first N categories get funded (C(5,2)=10 pairs) +const TOTAL_CATEGORIES = 17; // total budget categories in the system + +// Slider values for visual variety across the 10 pairs +const SLIDER_VALUES = [30, 70, 25, 60, 45, 75, 35, 55, 40, 65]; + +// ─── Test ───────────────────────────────────────────────────────────────────── test("full demo walkthrough", async ({ page }) => { - test.setTimeout(600_000); // 10 min total (wishocracy interactions take time) - - // ─── Section 1: Hook — Homepage hero (15s) ─── - await navigateTo(page, "/"); - await expect(page.locator("h1")).toBeVisible(); - await pause(2000); - - // Slow scroll down to reveal the hero - await page.evaluate(() => - window.scrollTo({ top: 400, behavior: "smooth" }), - ); - await pause(3000); - await page.evaluate(() => - window.scrollTo({ top: 800, behavior: "smooth" }), - ); - await pause(3000); - - // Scroll to the urgency stats - await page.evaluate(() => - window.scrollTo({ top: 1400, behavior: "smooth" }), - ); - await pause(4000); + test.setTimeout(720_000); // 12 min ceiling + + // ══════════════════════════════════════════════════════════════════════════ + // SECTION 1 — Hook: Homepage Hero + // "Your government is a misaligned superintelligence." + // ══════════════════════════════════════════════════════════════════════════ + await go(page, "/"); + await expect(page.locator("h1").first()).toBeVisible(); + await pause(2_000); + + // Slow hero scroll — let the animations fire + await scrollTo(page, 400); + await scrollTo(page, 900); + await scrollTo(page, 1_500); + await pause(SECTION_PAUSE); + + // ══════════════════════════════════════════════════════════════════════════ + // SECTION 2 — The Numbers: Political Dysfunction Tax + Invisible Graveyard + // "$604 on war for every $1 on cures. 150,000 dead per day." + // ══════════════════════════════════════════════════════════════════════════ + await scrollToText(page, "The Political Dysfunction Tax"); + await scrollToText(page, "The Invisible Graveyard"); + await scrollTo(page, await page.evaluate(() => { + const el = document.querySelector('h2'); + return el ? el.getBoundingClientRect().top + window.scrollY - 100 : window.scrollY + 400; + })); + + // Scroll through the graveyard stats cards + const graveyardSection = page.locator("text=The Invisible Graveyard").first(); + if ((await graveyardSection.count()) > 0) { + await graveyardSection.scrollIntoViewIfNeeded(); + await pause(BEAT_PAUSE); + // Scroll down through the four stat cards + for (let i = 0; i < 3; i++) { + await page.evaluate(() => window.scrollBy({ top: 250, behavior: "smooth" })); + await pause(1_000); + } + } + await pause(SECTION_PAUSE); + + // ══════════════════════════════════════════════════════════════════════════ + // SECTION 3 — War vs Cures Chart + // "22X more on war than curing all diseases combined" + // ══════════════════════════════════════════════════════════════════════════ + await scrollToText(page, "Your species spends"); + // Let the big number animation play + await pause(2_500); + await page.evaluate(() => window.scrollBy({ top: 400, behavior: "smooth" })); + await pause(BEAT_PAUSE); + await pause(SECTION_PAUSE); + + // ══════════════════════════════════════════════════════════════════════════ + // SECTION 4 — The 1% Treaty + // "Redirect 1% of the murder budget. 443 years → 36 years." + // ══════════════════════════════════════════════════════════════════════════ + await scrollToText(page, "The 1% Treaty"); + await page.evaluate(() => window.scrollBy({ top: 200, behavior: "smooth" })); + await pause(2_000); + await page.evaluate(() => window.scrollBy({ top: 400, behavior: "smooth" })); + await pause(2_000); + await page.evaluate(() => window.scrollBy({ top: 400, behavior: "smooth" })); + await pause(SECTION_PAUSE); + + // ══════════════════════════════════════════════════════════════════════════ + // SECTION 5 — Misconceptions: Myth vs Data + // "What your species believes vs what the spreadsheet says" + // ══════════════════════════════════════════════════════════════════════════ + await go(page, "/misconceptions"); + await pause(BEAT_PAUSE); + + // Show the summary header + await scrollTo(page, 0); + await pause(BEAT_PAUSE); + + // Scroll through a few myth cards + for (let i = 0; i < 4; i++) { + await page.evaluate(() => window.scrollBy({ top: 300, behavior: "smooth" })); + await pause(900); + } + + // Hover over a grade-F card for emphasis + const gradeF = page.locator('text="F"').first(); + if ((await gradeF.count()) > 0) { + await gradeF.scrollIntoViewIfNeeded(); + await gradeF.hover(); + await pause(1_500); + } + await pause(SECTION_PAUSE); + + // ══════════════════════════════════════════════════════════════════════════ + // SECTION 6 — Compare: Country Outcomes + // "Which countries are winning, and why" + // ══════════════════════════════════════════════════════════════════════════ + await go(page, "/compare"); + await pause(BEAT_PAUSE); + await scrollTo(page, 0); + + // Scroll through the health data table + for (let i = 0; i < 3; i++) { + await page.evaluate(() => window.scrollBy({ top: 400, behavior: "smooth" })); + await pause(1_000); + } + + // Toggle between health/drug tabs if they exist + const drugTab = page.getByRole("button", { name: /drug/i }); + if ((await drugTab.count()) > 0) { + await pause(BEAT_PAUSE); + await drugTab.click(); + await pause(1_500); + await page.evaluate(() => window.scrollBy({ top: 200, behavior: "smooth" })); + await pause(1_200); + } await pause(SECTION_PAUSE); - // ─── Section 2: Wishocracy — Full allocation flow (60s+) ─── - await navigateTo(page, "/wishocracy"); - await pause(2000); + // ══════════════════════════════════════════════════════════════════════════ + // SECTION 7 — Wishocracy: The Game + // "Allocate. Vote. Share your URL. Decentralise the lobby." + // ══════════════════════════════════════════════════════════════════════════ + await go(page, "/wishocracy"); + await pause(2_000); - // Step 2a: Click "LET'S GO" on the intro card + // ── 7a: LET'S GO ── const letsGoButton = page.getByRole("button", { name: /LET'S GO/i }); - await expect(letsGoButton).toBeVisible(); - await pause(1500); + await expect(letsGoButton).toBeVisible({ timeout: 8_000 }); + await pause(1_500); await letsGoButton.click(); - await pause(1500); + await pause(1_500); - // Step 2b: Category selection — fund the first N, skip the rest - // Categories appear one at a time with 300ms animation between each. + // ── 7b: Category selection ── for (let i = 0; i < TOTAL_CATEGORIES; i++) { if (i < CATEGORIES_TO_FUND) { - // Fund this category — click "More Than $0" - const fundButton = page.getByRole("button", { - name: /More Than \$0/i, - }); - await expect(fundButton).toBeVisible({ timeout: 5000 }); + const fundButton = page.getByRole("button", { name: /More Than \$0/i }); + await expect(fundButton).toBeVisible({ timeout: 5_000 }); await pause(600); await fundButton.click(); } else { - // Skip this category — click "$0" - // The button text is exactly "$0" — use exact text match - const skipButton = page.locator( - 'button:has-text("$0"):not(:has-text("More"))', - ); - await expect(skipButton).toBeVisible({ timeout: 5000 }); + const skipButton = page.locator('button:has-text("$0"):not(:has-text("More"))'); + await expect(skipButton).toBeVisible({ timeout: 5_000 }); await pause(400); await skipButton.click(); } - // Wait for animation to complete (300ms setTimeout + 300ms framer motion) - await pause(700); + await pause(700); // animation settle } - // Wait for completion card with animation - await pause(2000); - const startComparingButton = page.getByRole("button", { - name: /Start Comparing/i, - }); - await expect(startComparingButton).toBeVisible({ timeout: 10000 }); - await pause(1500); - await startComparingButton.click(); - await pause(2000); - - // Step 2c: Pairwise comparisons - // With 5 funded categories, we get C(5,2) = 10 pairs - const totalPairs = (CATEGORIES_TO_FUND * (CATEGORIES_TO_FUND - 1)) / 2; + // ── 7c: Start Comparing ── + await pause(2_000); + const startComparing = page.getByRole("button", { name: /Start Comparing/i }); + await expect(startComparing).toBeVisible({ timeout: 10_000 }); + await pause(1_500); + await startComparing.click(); + await pause(2_000); - // Slider allocations for visual variety — alternate between favoring left/right - const sliderValues = [30, 70, 25, 60, 45, 75, 35, 55, 40, 65]; + // ── 7d: Pairwise comparisons (10 pairs) ── + const totalPairs = (CATEGORIES_TO_FUND * (CATEGORIES_TO_FUND - 1)) / 2; - for (let pairIndex = 0; pairIndex < totalPairs; pairIndex++) { - // Wait for the slider to appear + for (let i = 0; i < totalPairs; i++) { const slider = page.locator('input[type="range"]'); - await expect(slider).toBeVisible({ timeout: 5000 }); - await pause(1000); + await expect(slider).toBeVisible({ timeout: 5_000 }); + await pause(900); - // Set the slider to a varied position - const targetValue = sliderValues[pairIndex % sliderValues.length]!; - await setSlider(page, targetValue); + await setSlider(page, SLIDER_VALUES[i % SLIDER_VALUES.length]!); await pause(800); - // Click "SUBMIT CHOICE" - const submitButton = page.getByRole("button", { - name: /SUBMIT CHOICE/i, - }); - await expect(submitButton).toBeVisible({ timeout: 5000 }); + const submitButton = page.getByRole("button", { name: /SUBMIT CHOICE/i }); + await expect(submitButton).toBeVisible({ timeout: 5_000 }); await pause(500); await submitButton.click(); - await pause(1200); + await pause(1_200); } - // Step 2d: Completion — show the results - await pause(3000); + // ── 7e: Results ── + await pause(3_000); - // Scroll to the allocation results - const allocationCard = page.locator("[data-complete-list]"); - if ((await allocationCard.count()) > 0) { - await allocationCard.scrollIntoViewIfNeeded(); - await pause(4000); - } + // Scroll to allocation results + const ipfsBadge = page + .locator('text="Content-addressed on IPFS"') + .or(page.locator('text="Storacha"')); + await scrollToLocator(page, ipfsBadge, 3_000); - // Look for "View Full Allocation" button - const viewAllocation = page.getByRole("button", { - name: /View Full Allocation/i, - }); + const viewAllocation = page.getByRole("button", { name: /View Full Allocation/i }); if ((await viewAllocation.count()) > 0) { await viewAllocation.click(); - await pause(4000); + await pause(3_500); } + await pause(SECTION_PAUSE); - // Look for IPFS badge on completion - const ipfsBadge = page - .locator('text="Content-addressed on IPFS"') - .or(page.locator('text="Storacha"')); - if ((await ipfsBadge.count()) > 0) { - await ipfsBadge.first().scrollIntoViewIfNeeded(); - await pause(3000); + // ══════════════════════════════════════════════════════════════════════════ + // SECTION 8 — Alignment: Politician Report Cards + // "Which politicians actually represent you? A single number." + // ══════════════════════════════════════════════════════════════════════════ + await go(page, "/alignment"); + await pause(2_000); + await scrollTo(page, 0); + + // Scroll through politician rankings + for (let i = 0; i < 3; i++) { + await page.evaluate(() => window.scrollBy({ top: 400, behavior: "smooth" })); + await pause(1_000); } - await pause(SECTION_PAUSE); - // ─── Section 3: Alignment — Politician Report Cards (20s) ─── - await navigateTo(page, "/alignment"); - await pause(2000); - await page.evaluate(() => - window.scrollTo({ top: 0, behavior: "smooth" }), - ); - await pause(3000); - - // Scroll through ranked politicians - await page.evaluate(() => - window.scrollTo({ top: 500, behavior: "smooth" }), - ); - await pause(3000); - - // Look for "Verified on IPFS" badge - const verifiedBadge = page.locator('text="Verified on IPFS"'); + // Highlight an IPFS verification badge + const verifiedBadge = page + .locator('text="Verified on IPFS"') + .or(page.locator('text="Citizen Alignment Score"')) + .first(); + await scrollToLocator(page, verifiedBadge); if ((await verifiedBadge.count()) > 0) { - await verifiedBadge.first().scrollIntoViewIfNeeded(); - await pause(2000); await verifiedBadge.first().hover(); - await pause(2000); + await pause(2_000); } - - await page.evaluate(() => - window.scrollTo({ top: 1000, behavior: "smooth" }), - ); - await pause(3000); await pause(SECTION_PAUSE); - // ─── Section 4: Hypercerts — Verifiable Attestations (25s) ─── - await navigateTo(page, "/transparency"); - await pause(2000); + // ══════════════════════════════════════════════════════════════════════════ + // SECTION 9 — Transparency: Hypercerts & Storacha + // "Every score immutable. Every claim auditable." + // ══════════════════════════════════════════════════════════════════════════ + await go(page, "/transparency"); + await pause(2_000); - // Show the pipeline - await page.evaluate(() => - window.scrollTo({ top: 300, behavior: "smooth" }), - ); - await pause(3000); + // Pipeline diagram + await scrollTo(page, 300); + await pause(2_000); - // Scroll to attestation records grid - await page.evaluate(() => - window.scrollTo({ top: 900, behavior: "smooth" }), - ); - await pause(4000); + // Attestation records grid + await scrollTo(page, 900); + await pause(2_000); - // Find and highlight IPFS CID links - const cidLinks = page.locator('a[href*="ipfs.storacha.link"]'); + // Hover over a Storacha CID link + const cidLinks = page.locator('a[href*="ipfs.storacha.link"], a[href*="storacha"]'); if ((await cidLinks.count()) > 0) { await cidLinks.first().scrollIntoViewIfNeeded(); - await pause(2000); + await pause(BEAT_PAUSE); await cidLinks.first().hover(); - await pause(2000); + await pause(2_000); } - // Scroll to preference snapshot - await page.evaluate(() => - window.scrollTo({ top: 1400, behavior: "smooth" }), - ); - await pause(3000); + // $WISH + IAB section + await scrollToText(page, "$WISH Token"); + await pause(2_000); + await page.evaluate(() => window.scrollBy({ top: 500, behavior: "smooth" })); + await pause(2_000); await pause(SECTION_PAUSE); - // ─── Section 5: Earth Optimization Prize (20s) ─── - await navigateTo(page, "/prize"); - await pause(2000); - - // Show the mechanism steps - await page.evaluate(() => - window.scrollTo({ top: 400, behavior: "smooth" }), - ); - await pause(3000); - await page.evaluate(() => - window.scrollTo({ top: 900, behavior: "smooth" }), - ); - await pause(3000); - - // Show donation/wallet section - await page.evaluate(() => - window.scrollTo({ top: 1400, behavior: "smooth" }), - ); - await pause(3000); - - // Show the Three Mechanisms section - const threeMechanisms = page.locator( - 'text="Three Mechanisms. One System."', - ); - if ((await threeMechanisms.count()) > 0) { - await threeMechanisms.scrollIntoViewIfNeeded(); - await pause(4000); - } + // ══════════════════════════════════════════════════════════════════════════ + // SECTION 10 — Prize Pool: The Free Option + // "You either get a healthier planet, or you get your money back + 17%." + // ══════════════════════════════════════════════════════════════════════════ + await go(page, "/prize"); + await pause(2_000); - // Show pool status - const poolStatus = page.locator('text="Pool Status"'); - if ((await poolStatus.count()) > 0) { - await poolStatus.scrollIntoViewIfNeeded(); - await pause(3000); - } - await pause(SECTION_PAUSE); + // Mechanism steps + await scrollTo(page, 400); + await pause(2_000); - // ─── Section 6: $WISH Token + IAB (25s) ─── - await navigateTo(page, "/transparency"); - await pause(1000); + await scrollToText(page, "Three Mechanisms"); + await pause(2_500); - // Scroll to $WISH Token & UBI section - const wishSection = page.locator('text="$WISH Token & UBI"'); - if ((await wishSection.count()) > 0) { - await wishSection.scrollIntoViewIfNeeded(); - await pause(4000); - } + await page.evaluate(() => window.scrollBy({ top: 400, behavior: "smooth" })); + await pause(1_500); - // Scroll through mechanism cards - await page.evaluate(() => - window.scrollTo({ top: 2000, behavior: "smooth" }), - ); - await pause(4000); - - // Show technology stack - const techStack = page.locator('text="Technology Stack"'); - if ((await techStack.count()) > 0) { - await techStack.scrollIntoViewIfNeeded(); - await pause(3000); - } + // Pool status / smart contract details + await scrollToText(page, "Pool Status"); + await pause(2_500); + + // Deposit / wallet section + await page.evaluate(() => window.scrollBy({ top: 500, behavior: "smooth" })); + await pause(2_000); await pause(SECTION_PAUSE); - // ─── Section 7: Architecture (15s) ─── - await navigateTo(page, "/about"); - await pause(1000); - await page.evaluate(() => - window.scrollTo({ top: 400, behavior: "smooth" }), - ); - await pause(2000); - await page.evaluate(() => - window.scrollTo({ top: 1000, behavior: "smooth" }), - ); - await pause(2000); - - const economicSystem = page.locator('text="The Economic System"'); - if ((await economicSystem.count()) > 0) { - await economicSystem.scrollIntoViewIfNeeded(); - await pause(3000); + // ══════════════════════════════════════════════════════════════════════════ + // SECTION 11 — Outcomes & Studies: The Data Engine + // "Which policies actually worked. Not which ones were popular." + // ══════════════════════════════════════════════════════════════════════════ + await go(page, "/outcomes"); + await pause(BEAT_PAUSE); + await scrollTo(page, 0); + + // Browse a few outcome cards + for (let i = 0; i < 3; i++) { + await page.evaluate(() => window.scrollBy({ top: 350, behavior: "smooth" })); + await pause(900); } - const research = page.locator('text="Research"'); - if ((await research.count()) > 0) { - await research.scrollIntoViewIfNeeded(); - await pause(3000); + // Click into one outcome for depth + const firstOutcomeCard = page.locator('a[href*="/outcomes/"]').first(); + if ((await firstOutcomeCard.count()) > 0) { + await firstOutcomeCard.scrollIntoViewIfNeeded(); + await pause(BEAT_PAUSE); + await firstOutcomeCard.click(); + await page.waitForLoadState("networkidle"); + await pause(2_000); + await page.evaluate(() => window.scrollBy({ top: 400, behavior: "smooth" })); + await pause(1_500); + await page.evaluate(() => window.scrollBy({ top: 400, behavior: "smooth" })); + await pause(1_500); } await pause(SECTION_PAUSE); - // ─── Section 8: Close (15s) ─── - await navigateTo(page, "/"); - await pause(2000); - await expect(page.locator("h1")).toBeVisible(); - await pause(3000); - - // Final scroll to economic system section on homepage - const diagnosisFree = page - .locator('text="Diagnosis Is Free"') - .or(page.locator('text="Funding Isn"')); - if ((await diagnosisFree.count()) > 0) { - await diagnosisFree.first().scrollIntoViewIfNeeded(); - await pause(4000); - } - - // End on the hero - await page.evaluate(() => - window.scrollTo({ top: 0, behavior: "smooth" }), - ); - await pause(4000); + // ══════════════════════════════════════════════════════════════════════════ + // SECTION 12 — Close: Back to Homepage Hero + // "Alignment software for the most powerful AIs on your planet — + // the ones made of people." + // ══════════════════════════════════════════════════════════════════════════ + await go(page, "/"); + await scrollTo(page, 0); + await expect(page.locator("h1").first()).toBeVisible(); + await pause(3_000); + + // Final slow hero scroll + await scrollTo(page, 400); + await scrollTo(page, 900); + + // Land on the IAB / Prize sections for the closer + await scrollToText(page, "Incentive Alignment"); + await pause(2_000); + await page.evaluate(() => window.scrollBy({ top: 300, behavior: "smooth" })); + await pause(2_000); + + // Return to top for end card + await scrollTo(page, 0); + await pause(4_000); });