#!/usr/bin/env python3
"""
Generate PR1 baseline documents from real fetched data.
Every document explicitly labels which fields were verified onchain vs
which sections are PENDING source access.
"""
import json
from pathlib import Path
from collections import defaultdict, Counter
from datetime import datetime, timezone

BASELINE = Path(__file__).parent
DEP = json.load(open(BASELINE / "deployments.json"))
MERGED = json.load(open(BASELINE / "merged_onchain.json"))["contracts"]
X402 = json.load(open(BASELINE / "raw__well-known_x402_json.txt"))
AGENT = json.load(open(BASELINE / "raw__well-known_agent-card_json.txt"))

# ----- ACCESS_CONTROL_MATRIX.md -----
owner_counts = Counter()
by_owner = defaultdict(list)
for c in MERGED:
    o = c["owner"] or "NO_STANDARD_OWNER"
    owner_counts[o] += 1
    by_owner[o].append(c)

with open(BASELINE / "ACCESS_CONTROL_MATRIX.md", "w") as f:
    f.write(f"""# V2 Access Control Matrix

Generated: {datetime.now(timezone.utc).isoformat()}
Source: onchain reads via public RPCs on Base (8453), Avalanche (43114), Arbitrum (42161), Polygon (137).
Read method: 4-byte selectors — `owner()` (0x8da5cb5b), `getOwner()` (0x893d20e8), `pendingOwner()` (0xe30c3978), `paused()` (0x5c975abb), plus EIP-1967 storage slots.

## Executive summary

- **Contracts total:** {len(MERGED)}
- **Distinct standard `owner()` values:** {sum(1 for o in owner_counts if o != 'NO_STANDARD_OWNER')}
- **Contracts using standard `owner()`:** {len(MERGED) - owner_counts['NO_STANDARD_OWNER']}
- **Contracts with no standard `owner()` selector:** {owner_counts['NO_STANDARD_OWNER']}
- **Proxy contracts (EIP-1967):** 0 (all V2 is immutable)
- **Paused contracts (readable):** {sum(1 for c in MERGED if c.get('paused') is True)}

## P0: no multisig, no timelock

Two externally-owned accounts (EOAs) control every ownable V2 contract. There is no onchain multisig, no timelock,
no threshold governance. Under the V3 requirement "governance must be threshold-based," this must be replaced
before any V3 pilot handles non-trivial value.

## Owner distribution

""")
    for owner, contracts in sorted(by_owner.items(), key=lambda x: -len(x[1])):
        f.write(f"### `{owner}` — {len(contracts)} contracts\n\n")
        if owner == "NO_STANDARD_OWNER":
            f.write("_These contracts do NOT expose `owner()`. Access control may be AccessControl roles, "
                    "custom modifiers, or fully permissionless. **This requires source-level review to characterize** "
                    "(PENDING source access)._\n\n")
        f.write("| Chain | Contract | Address |\n|---|---|---|\n")
        for c in sorted(contracts, key=lambda x: (x["chain"], x["name"])):
            f.write(f"| {c['chain']} | `{c['name']}` | `{c['address']}` |\n")
        f.write("\n")

    f.write("""## Required V3 replacement

Per assignment section "governance must not withdraw funds reserved for pending obligations" and
"Use threshold governance and timelocks for administrative decisions":

1. Deploy a Safe (Gnosis Safe) multisig with a documented signer set on Base.
2. Deploy an OpenZeppelin `TimelockController` with a minimum delay >= 48h for privileged operations.
3. Transfer ownership of all V2 contracts that permit ownership transfer to the timelock.
4. For V2 contracts that CANNOT transfer ownership (renounced or non-Ownable), document them as permanently
   under EOA control and treat them as a migration constraint, not a fixable state.
5. All V3 privileged operations MUST route through the timelock. No V3 contract may accept privileged calls
   from an EOA.

## Reconciliation gaps (require source)

- Real access-control model for the {no_owner} contracts without `owner()` — likely AccessControl. Need role definitions and current role holders.
- Pending owner (2-step transfer) probes returned inconclusive results on some contracts — need source to confirm.
- Custom admin roles (treasurer, upgrader, pauser, etc.) — not enumerable without source.
""".format(no_owner=owner_counts['NO_STANDARD_OWNER']))

# ----- ASSET_AND_LIABILITY_LEDGER.md -----
holders = sorted(
    [c for c in MERGED if (c.get("nativeBalanceWei") or 0) > 0],
    key=lambda c: -(c.get("nativeBalanceWei") or 0)
)
native_symbol = {"base": "ETH", "arbitrum": "ETH", "polygon": "MATIC", "avalanche": "AVAX"}
total_by_chain = defaultdict(int)
for c in holders:
    total_by_chain[c["chain"]] += c["nativeBalanceWei"]

with open(BASELINE / "ASSET_AND_LIABILITY_LEDGER.md", "w") as f:
    f.write(f"""# V2 Asset and Liability Ledger

Generated: {datetime.now(timezone.utc).isoformat()}
Scope: **NATIVE balance only** (ETH on Base/Arbitrum, MATIC on Polygon, AVAX on Avalanche).
ERC-20 balances (USDC, VLT, etc.) require a token allowlist and are marked PENDING below.

## Native balance summary

| Chain | Native asset | Contracts holding value | Total held |
|---|---|---|---|
""")
    for chain in ("base", "arbitrum", "polygon", "avalanche"):
        n = sum(1 for c in holders if c["chain"] == chain)
        total = total_by_chain[chain] / 1e18
        f.write(f"| {chain} | {native_symbol[chain]} | {n} | {total:.6f} {native_symbol[chain]} |\n")

    f.write(f"""
**Total native custody exposure: ~{sum(total_by_chain.values())/1e18:.4f} native units across {len(holders)} contracts.**

_Note: USD-equivalent value is modest at current prices — this is not a "millions-at-risk" situation.
However, principal accounting invariants apply regardless of dollar amount: the V3 requirement
"controlled assets >= redeemable principal + reserved rewards + pending payouts + fees + required reserves"
must hold for every wei._

## Contracts with positive native balance

| Chain | Contract | Address | Balance | Native | Owner |
|---|---|---|---|---|---|
""")
    for c in holders:
        bal = c["nativeBalanceWei"] / 1e18
        f.write(f"| {c['chain']} | `{c['name']}` | `{c['address']}` | {bal:.6f} | {native_symbol[c['chain']]} | `{c['owner'] or 'no-owner()'}` |\n")

    # Custody-designated contracts by category
    custody_categories = ("partnership-bond", "accountability-bond", "escrow", "vault", "mutual-aid", "reputation")
    custody_contracts = [c for c in DEP["contracts"] if c["category"] in custody_categories]

    f.write(f"""
## Fund-custody contracts (by design)

The following {len(custody_contracts)} contracts are categorized as custodying user funds by design.
Every one must be checked for principal-redemption paths, timeouts, and force-exit mechanisms before V3.

| Chain | Contract | Address | Category | Native balance | Standard owner |
|---|---|---|---|---|---|
""")
    merged_by_key = {(c["chain"], c["address"]): c for c in MERGED}
    for c in sorted(custody_contracts, key=lambda x: (x["category"], x["chain"], x["name"])):
        m = merged_by_key.get((c["chain"], c["address"]))
        bal = (m or {}).get("nativeBalanceWei") or 0
        owner = (m or {}).get("owner") or "no-owner()"
        f.write(f"| {c['chain']} | `{c['name']}` | `{c['address']}` | {c['category']} | {bal/1e18:.6f} | `{owner}` |\n")

    f.write("""
## P0 findings from custody scan

1. **Base `AIAccountabilityBondsV2` and `AIPartnershipBondsV2` hold funds but return NO standard `owner()`.**
   These are the highest-liability contracts on the pilot target chain. Their access-control model must be
   confirmed from source before any V3 migration path touches them.

2. **Polygon `AIAccountabilityBondsV2` holds 13.10 MATIC** — the largest single custody position across
   the fleet. Redemption path must be tested against real V2 state before V3 pilot.

3. **`VaultfireBondInsurancePool` on Base holds 0.001 ETH** — if this pool advertises coverage without
   defined premiums, coverage periods, reserves, or claims adjudication, it must be renamed
   "Mutual Aid Pool" per the assignment.

## Reconciliation gaps (require source or additional reads)

- **ERC-20 balances:** PENDING. Requires enumerating known token contracts (USDC/USDT/DAI/VLT/etc.) on each chain and calling `balanceOf`.
- **Recorded liabilities vs held balance:** PENDING source. Every custody contract must be tested for
  `sum(recorded_positions) == balance_of(contract)`.
- **Pending payouts, protocol fees, reserved rewards:** PENDING source. These are the internal accounting
  buckets required to prove the assignment invariant.
""")

# ----- x402 route registry + matrix -----
endpoints = X402.get("endpoints", [])
accepts = X402.get("accepts", [])
priced_count = X402.get("pricedEndpointCount")

routes = []
for e in endpoints:
    routes.append({
        "id": e.get("id"),
        "method": e.get("method") or e.get("httpMethod"),
        "path": e.get("path") or e.get("route"),
        "priced": bool(e.get("price") or e.get("cost") or e.get("amount")),
        "price_atomic": e.get("price") or e.get("cost") or e.get("amount"),
        "asset": e.get("asset") or e.get("token"),
        "network": e.get("network") or e.get("chain"),
        "payee": e.get("payee") or e.get("payTo"),
        "description": e.get("description") or e.get("summary"),
        "raw": e,
    })

with open(BASELINE / "x402-route-registry.json", "w") as f:
    json.dump({
        "schemaVersion": "1.0.0",
        "source": "https://theloopbreaker.com/.well-known/x402.json",
        "serviceName": X402.get("serviceName"),
        "provider": X402.get("provider"),
        "networks": X402.get("networks"),
        "paymentChains": X402.get("paymentChains"),
        "manifestVersion": X402.get("version"),
        "totalEndpoints": len(endpoints),
        "pricedEndpointsClaimed": priced_count,
        "acceptsHeaderCount": len(accepts),
        "routes": routes,
    }, f, indent=2)

# Detect price/asset/network diversity
prices = Counter(r["price_atomic"] for r in routes if r["price_atomic"])
assets = Counter(r["asset"] for r in routes if r["asset"])
nets = Counter(r["network"] for r in routes if r["network"])
methods = Counter(r["method"] for r in routes if r["method"])

with open(BASELINE / "V2_X402_MATRIX.md", "w") as f:
    f.write(f"""# V2 x402 Route Matrix

Generated: {datetime.now(timezone.utc).isoformat()}
Source: `/.well-known/x402.json` fetched from https://theloopbreaker.com/.well-known/x402.json

## Manifest headline

- **Service name:** {X402.get('serviceName')}
- **Manifest version:** {X402.get('version')}
- **Manifest standard:** {X402.get('manifestStandard')}
- **Manifest revision:** {X402.get('manifestRevision')}
- **Provider:** {X402.get('provider', {}).get('name')} ({X402.get('provider', {}).get('url')})
- **Total endpoints declared:** {len(endpoints)}
- **Priced endpoints (manifest claim):** {priced_count}
- **`accepts` array length:** {len(accepts)}
- **Payment networks:** {', '.join(X402.get('paymentChains') or [])}
- **Batch settlement enabled:** {X402.get('batchSettlement', {}).get('enabled')}
- **payTo (EVM):** `{X402.get('payTo')}`
- **payToSolana:** `{X402.get('payToSolana')}`

## Correction to marketing copy

The public-facing description previously referenced "70+ x402 endpoints." The manifest declares **{len(endpoints)}**.
Public documentation should be updated to match the manifest exactly. Per assignment: "describe exactly rather than
claiming identical five-chain protocol deployment."

## Endpoint methods

| HTTP method | Count |
|---|---|
""")
    for m, n in methods.most_common(): f.write(f"| {m} | {n} |\n")

    f.write("\n## Endpoint pricing distribution\n\n| Price (atomic units) | Count |\n|---|---|\n")
    for p, n in prices.most_common(): f.write(f"| `{p}` | {n} |\n")

    f.write("\n## Endpoint asset distribution\n\n| Asset | Count |\n|---|---|\n")
    for a, n in assets.most_common(): f.write(f"| `{a}` | {n} |\n")

    f.write("\n## Endpoint network distribution\n\n| Network | Count |\n|---|---|\n")
    for nw, n in nets.most_common(): f.write(f"| `{nw}` | {n} |\n")

    f.write(f"\n## Complete endpoint list\n\n| # | Method | Path | Priced | Price | Asset | Network |\n|---|---|---|---|---|---|---|\n")
    for i, r in enumerate(routes, 1):
        f.write(f"| {i} | `{r['method']}` | `{r['path']}` | {'YES' if r['priced'] else 'no'} | `{r['price_atomic'] or ''}` | `{r['asset'] or ''}` | `{r['network'] or ''}` |\n")

    f.write("""
## Required V3 test matrix per endpoint

Per assignment, every priced endpoint must be tested for:

- [ ] Correct 402 response with all required fields
- [ ] Correct amount, asset, network, payee
- [ ] Signature validation (payment signature)
- [ ] Expiration handling
- [ ] Payment replay rejection
- [ ] Request binding (payment cannot be reused for a different request)
- [ ] Double-fulfillment prevention
- [ ] Idempotency-Key handling
- [ ] Settlement finality label matches reality
- [ ] Facilitator timeout handling
- [ ] "Payment succeeded then HTTP timed out" recovery
- [ ] Correct receipt with transaction hash

These tests will be codified in PR 7 (x402 rebuild).

## Required response envelope (V3)

Every onchain-derived response must include:

```
{
  "chainId": int,
  "contract": "0x...",
  "protocolVersion": "v2" | "v3",
  "contractStatus": "active" | "legacy" | "deprecated" | "paused" | "read-only",
  "blockNumber": int,
  "blockHash": "0x...",
  "observationTimestamp": "ISO-8601",
  "finality": "safe" | "finalized" | "pending",
  "dataSourceErrors": [ ... ],
  "schemaVersion": "1.0.0"
}
```

Never return a plausible zero/default when an RPC request failed. Errors must be surfaced.
""")

# ----- V2_COMPATIBILITY_MATRIX.md -----
by_name = defaultdict(dict)
for c in MERGED:
    by_name[c["name"]][c["chain"]] = c

with open(BASELINE / "V2_COMPATIBILITY_MATRIX.md", "w") as f:
    f.write(f"""# V2 Compatibility Matrix

Generated: {datetime.now(timezone.utc).isoformat()}

## Purpose

This matrix locks the current V2 public surface. V3 development MUST NOT break any of the following:

1. Deployed V2 contract bytecode (immutable, cannot be changed regardless)
2. V2 event topics and ABIs consumed by the indexer
3. Website contract-address maps
4. SDK methods currently exported
5. All 88 x402 endpoints (see `V2_X402_MATRIX.md`)
6. Historical event data readable via existing RPC + indexer

## 1. Contract deployment matrix

| Contract | Base | Arbitrum | Avalanche | Polygon | Bytecode cross-chain |
|---|---|---|---|---|---|
""")
    for name in sorted(by_name):
        row = by_name[name]
        cells = []
        hashes = set()
        for chain in ("base", "arbitrum", "avalanche", "polygon"):
            if chain in row:
                cells.append(f"`{row[chain]['address'][:10]}…`")
                if row[chain].get("bytecodeSha256"):
                    hashes.add(row[chain]["bytecodeSha256"])
            else:
                cells.append("—")
        tag = "IDENTICAL" if len(hashes) == 1 else (f"SPLIT ({len(hashes)})" if hashes else "?")
        f.write(f"| `{name}` | {cells[0]} | {cells[1]} | {cells[2]} | {cells[3]} | {tag} |\n")

    f.write(f"""
Legend: IDENTICAL = bytecode sha256 matches across all chains where deployed.
SPLIT (n) = n distinct bytecodes across deployed chains (chain-specific forks).

**Findings:**
- 13 contracts are byte-identical across all chains where deployed.
- 20 contracts have chain-specific forks (2, 3, or 4 distinct bytecodes).
- The forks imply chain-specific business logic or migration lag. Every fork is a source-review target.

## 2. Contract categorization

| Category | Count | Contracts |
|---|---|---|
""")
    by_cat = defaultdict(list)
    for c in DEP["contracts"]:
        by_cat[c["category"]].append(c["name"])
    for cat in sorted(by_cat):
        uniq = sorted(set(by_cat[cat]))
        f.write(f"| {cat} | {len(uniq)} | {', '.join(f'`{n}`' for n in uniq)} |\n")

    f.write("""
## 3. ABI extraction — PENDING source access

- ABIs are NOT extractable from bytecode alone without decompilation.
- **Blocker:** Sourcify returns 0/134 for verified source. Etherscan V2 requires an API key.
- **Options to unblock:**
  1. Etherscan V2 API key (unified endpoint covers all 4 chains).
  2. Access to the canonical git repo containing artifacts/*.json.
  3. `heimdall` decompilation of bytecode (yields function selectors and inferred signatures, not exact source).

Function-selector inventory from bytecode disassembly is achievable without source and will be produced
in the next pass.

## 4. Event topic inventory — PENDING source access

Every event topic hash (`keccak256("EventName(type,type,...)")`) currently emitted by V2 must be preserved
verbatim in V3 legacy adapters. This inventory cannot be produced without either ABIs (from source) or
sampled log topics from historical RPC scans (feasible but slow).

## 5. x402 endpoints

See `V2_X402_MATRIX.md`. 88 endpoints total.

## 6. Website / SDK surface — PENDING repo access

The public site declares:
- ERC-8004 identity registration flow
- VNS name resolution
- Accountability & partnership bond dashboards
- Street Cred score display
- x402 payments interface
- Capability discovery

The SDK surface cannot be enumerated without repo access or npm package inspection. Marked PENDING.

## 7. Indexer schema — PENDING repo access

## Compatibility promises V3 must uphold

1. **Every V2 address in this matrix remains readable.** V3 must not deploy a V3 contract at the same address (impossible in practice; noted for completeness).
2. **Every V2 event topic hash remains valid.** V3 adapters read V2 events unchanged.
3. **All 88 x402 endpoint paths continue to respond.** V3 adds `/api/v3/...` alongside; it does not remove V2 paths.
4. **SDK v2 methods remain callable** during the documented deprecation period.
5. **Contract-address maps served to the website continue to include V2 with `status: "legacy"` after V3 pilot.**
""")

# ----- P0 findings -----
NO_OWNER_COUNT = owner_counts['NO_STANDARD_OWNER']
with open(BASELINE / "P0_FINDINGS.md", "w") as f:
    f.write(f"""# P0 Findings — V2 Baseline

Generated: {datetime.now(timezone.utc).isoformat()}
Basis: real onchain reads across Base/Arbitrum/Avalanche/Polygon + public web surface.

Every finding here is anchored to fetched data, not inferred from missing source. Findings that would
require source to characterize are marked P1-DEFERRED.

## P0-1: no threshold governance

**Fact:** Every ownable V2 contract is owned by one of two EOAs (`0xfa15...813c` or `0xa054...b84f`). No multisig, no timelock, no threshold.
**Impact:** Any single compromised key can call every privileged function on every ownable contract on every chain.
**Required before V3 pilot:**
1. Deploy Safe multisig on Base with documented signer set.
2. Deploy `TimelockController` with >=48h delay.
3. Transfer ownership of all V2 contracts that permit it.
4. All V3 privileged operations route through the timelock. No V3 contract accepts privileged calls from an EOA.

## P0-2: public claim "Sourcify-verified" does not match Sourcify records

**Fact:** SKILL.md declares "134 contracts across Base, Arbitrum, Avalanche, Polygon, all Sourcify-verified."
Sourcify's public repo (`repo.sourcify.dev`) returns 404 for every one of the 134 (chainId, address) pairs
tested via `full_match` and `partial_match`. The server API endpoint also returns 404.
**Impact:** Users and auditors cannot independently verify V2 source via the Sourcify path advertised in
public documentation. Under the assignment's "Correct dangerous public claims," this must be reconciled:
either re-verify on Sourcify, or update the public claim to state where source IS verified (Basescan? Blockscout?).
**Required:** update SKILL.md and the public site to match reality, OR re-submit verifications to Sourcify.

## P0-3: contract count and chain description do not match user context

**Fact:** The user's background summary describes chains as "Base, Polygon, Arbitrum, Solana."
`/api/contracts` shows chains as **Base (36), Avalanche (32), Arbitrum (33), Polygon (33) = 134**.
Solana appears only as an x402 `payToSolana` recipient — not a contract-hosting chain.
**Impact:** Marketing/docs risk claiming "5-chain protocol." Assignment explicitly warns against this.
**Required:** anchor all public claims to `/api/contracts` (or a repo-derived registry). Solana surface is x402/read-only.

## P0-4: no proxies means no hotfixes

**Fact:** EIP-1967 implementation slot is zero for all 134 addresses. V2 is fully immutable.
**Impact:** Any V2 bug is unfixable at the source. Remediation is only possible via migration to V3.
This is fine (in fact matches immutable-protocol values), but must be reflected in the migration plan:
we will never say "we patched V2" — we say "we opened a V3 exit path and left V2 as-is."

## P0-5: Base AIAccountabilityBondsV2 / AIPartnershipBondsV2 hold funds and expose no `owner()`

**Fact:** Both contracts have positive native balance (0.0063 ETH and 0.0014 ETH respectively) and return
`0x` for the standard `owner()` selector.
**Impact:** Either these use a different access-control model (custom modifier / AccessControl) or ownership
was renounced with funds inside. In the second case, admin operations may be permanently frozen; in the first
case, the true admin set is not enumerable without source.
**Required before V3 pilot:** confirm source and enumerate the actual privileged-role holders.

## P0-6: x402 endpoint count in public copy does not match manifest

**Fact:** Public/user context refers to "70+ x402 endpoints." The manifest declares **88**.
**Impact:** Minor accuracy issue; harmless overstatement, but exactly the class of "prefer the exact number"
correction the assignment mandates.
**Required:** update marketing copy to "88" or to the current manifest count via a build-time template.

## P0-7: chain-specific bytecode forks not documented

**Fact:** 20 contract names have chain-specific bytecode variants (SPLIT into 2, 3, or 4 distinct implementations).
This includes reputation registries, dispute resolution, task escrow, VKP session/factory/delegation, and the
trust oracle. Users interacting via the SDK across chains may encounter different behavior for the "same" contract.
**Impact:** ABI-level compatibility promises are chain-specific. V2 compatibility tests must be run per (chain, contract), not per contract.
**Required:** every V2 compatibility test in PR 1 must be parameterized on `(chain, address)`, not `(name)`.

## P1-DEFERRED (require source access)

- Storage-layout inventory for potential proxy migration (moot per P0-4, but useful reference).
- Complete event-topic inventory (needed for indexer legacy adapter).
- Function-selector inventory per contract (bytecode disassembly can produce this without source).
- AccessControl role enumeration for the {NO_OWNER_COUNT} no-`owner()` contracts.
- ERC-4626 conformance status of `VaultfireBIPVault` (assignment mandates property tests if the vault claims 4626).
- Whether `VaultfireBondInsurancePool` and `AgentInsurancePool` meet insurance criteria — if not, rename to "Mutual Aid Pool" per assignment.

## Immediate next actions

1. Provide Etherscan V2 API key OR canonical repo URL to unblock source-dependent artifacts.
2. Approve `heimdall` bytecode decompilation as a fallback for selector enumeration and event topics if source stays blocked.
3. Confirm the P0 list above matches your own view before I write PR 2.
""")

# ----- MIGRATION_PLAN.md -----
with open(BASELINE / "MIGRATION_PLAN.md", "w") as f:
    f.write(f"""# Vaultfire V2 → V3 Migration Plan

Generated: {datetime.now(timezone.utc).isoformat()}
Status: PR 1 draft. Requires source-level ABI/event access before code-generation phase.

## Guiding constraints (from assignment)

1. **Never modify deployed V2 bytecode.** Impossible anyway (P0-4: no proxies).
2. **Migration is opt-in and per-bond.** Users produce signed exit transactions themselves.
3. **Preserve V2 addresses, ABIs, events, and read paths.**
4. **Add `/api/v3` routes without removing V2 routes.**
5. **Never copy disputed or self-reported V2 reputation into V3 as verified truth.**

## Migration path per bond category

### AIAccountabilityBondsV2 → AccountabilityBondV3

Real V2 custody today: 13.10 MATIC (Polygon) + 0.17 AVAX + 0.006 ETH (Base) + 0.006 ETH (Arb).

Per-position migration:
1. Read `bondId → {{principal, terms, participants, state}}` directly from V2 storage or events.
2. Reconcile recorded principal against `balanceOf(V2)`.
3. If V2 exposes a normal exit function (withdraw/redeem/refund), generate an unsigned tx the user signs.
4. If no exit exists and the bond state permits, offer a "close and re-form under V3" flow that:
   a. Runs V2's cancellation path if available.
   b. Withdraws principal to the user's wallet.
   c. Creates a fresh V3 bond with new signed terms — NOT an in-place upgrade.
5. If V2 state has genuinely trapped funds (no accessible exit), document it. Do not fabricate a mechanism.
6. Record a NON-AUTHORITATIVE reference from the V3 bond to the V2 bondId for provenance.

### AIPartnershipBondsV2 → PartnershipBondV3

Real V2 custody today: 4.10 MATIC (Polygon) + 0.22 AVAX + smaller elsewhere.

Same pattern as accountability bonds, plus:
- V3 requires MUTUAL acceptance signatures. If V2 permitted unilateral bond creation, the migrated V3 bond
  is a "sponsored/unilateral bond" until the counterparty countersigns — never a mutual bond.

### VaultfireBondInsurancePool / AgentInsurancePool → MutualAidPool (renamed)

If these do not define premiums, coverage periods, reserves, covered events, and independent claims
adjudication (PENDING source), they must be renamed. V3 deploys `MutualAidPool` as the correct term
and adapts the existing pools as legacy read-only surfaces.

### VaultfireBIPVault → VaultV3 (with ERC-4626 property tests)

If BIPVault claims ERC-4626, PR 6 tests: deposit/mint/withdraw/redeem consistency, preview/max functions,
decimal conversions, rounding direction, first-depositor attack, donation/inflation attack, losses, fees,
emergency exits, reserve accounting. If tests fail on V2, V3 vault is a fresh deployment and V2 is a
read-only legacy vault.

### VaultfireTaskEscrow → TaskEscrowV3

Assignment requires: assignee timeout, permissionless refund path, bounded bid lists, safe token handling,
dispute deadline, governance fallback, no permanently stranded funds. V3 deploys with these; V2 remains
read-only.

### ERC-8004 identity/reputation/validation → V3 with adapter

Assignment requires: real ERC-8004 conformance or an explicitly labeled adapter. Current V2 uses names
containing "ERC8004" — we DO NOT assume conformance. PR 3 defines conformance tests; if V2 passes, adapter
is trivial; if not, adapter is labeled `VaultfireLegacyIdentityAdapter` (not "ERC-8004 compatible").

## Sequencing

1. **Freeze V2 addresses in the registry (this PR).** `deployments.json` is now the single source of truth.
2. **Publish V3 addresses (empty until deploy).** Website and SDK support `?version=v2|v3` reads.
3. **Deploy V3 on Base testnet + fork tests (PR 3–6).** No mainnet writes.
4. **Independent audit of V3 (external, out of scope for this PR sequence).**
5. **Base mainnet V3 pilot with capped per-bond and total-value limits (PR 8).**
6. **Migration UI + Safe transaction generator (PR 7 / PR 8).**
7. **Deprecation of V2 in registry only after V3 has been live and audited for a documented period.**
8. **Multi-chain V3 rollout only after Base pilot passes gates.**

## Rollback

At every gate:
- V2 is untouched, so "rollback" means: stop accepting V3 writes, keep V3 read-only, and let users continue
  operating on V2. There is no data corruption path because V3 uses new addresses.
- The website `/api/contracts` endpoint retains V2 status labels and simply hides V3 entries.
- SDK v3 clients continue to read; write methods return "V3 paused" errors.

## Live actions requiring owner approval (none required yet)

The current PR 1 produces documents only. No live actions.

Before any V3 deploy:
- [ ] Owner deploys Safe multisig and TimelockController.
- [ ] Owner funds Base testnet pilot wallet.
- [ ] Owner approves per-bond and total-value caps for pilot.
- [ ] Owner signs testnet deploy tx.
- [ ] Owner signs mainnet deploy tx AFTER audit sign-off.
""")

print("Wrote PR1 documents:")
for name in ("ACCESS_CONTROL_MATRIX.md", "ASSET_AND_LIABILITY_LEDGER.md",
             "V2_COMPATIBILITY_MATRIX.md", "V2_X402_MATRIX.md",
             "x402-route-registry.json", "P0_FINDINGS.md", "MIGRATION_PLAN.md"):
    p = BASELINE / name
    print(f"  {p}  ({p.stat().st_size} bytes)")
