"""Generate a Markwhen timeline (.mw) for the TEC retrospective. Inputs: data/onchain/cv_proposals.csv (id, block, amount_requested, link, status, executed_block) data/onchain/cv_proposal_categories.json (id -> category tag, optional) Output: output/tec_retrospective.mw Anchoring: Gnosis block 20086948 == 2022-01-27 (CV proposal #1 per synthesis), block time ~5.0s. Close enough for a retrospective timeline. """ from __future__ import annotations import csv import json from datetime import datetime, timedelta, timezone from pathlib import Path ROOT = Path(__file__).resolve().parent.parent PROPOSALS = ROOT / "data" / "onchain" / "cv_proposals.csv" CATEGORIES = ROOT / "data" / "onchain" / "cv_proposal_categories.json" OUT = ROOT / "output" / "tec_retrospective.mw" ANCHOR_BLOCK = 20086948 ANCHOR_DATE = datetime(2022, 1, 27, tzinfo=timezone.utc) GNOSIS_BLOCK_SECONDS = 5.0 def block_to_date(block: int) -> datetime: delta = (block - ANCHOR_BLOCK) * GNOSIS_BLOCK_SECONDS return ANCHOR_DATE + timedelta(seconds=delta) def load_categories() -> dict[str, str]: if not CATEGORIES.exists(): return {} raw = json.loads(CATEGORIES.read_text()) if isinstance(raw, dict): return {str(k): str(v) for k, v in raw.items()} return {} HEADER = """--- title: Token Engineering Commons — Retrospective view: timeline timezone: UTC #hatch: blue #abc: orange #cv: green #shock: red #migration: purple #shutdown: gray --- // Generated by scripts/gen_timeline.py // Edit macro events here; CV proposal section is auto-appended. section Origins #hatch 2021-02-01 / 2021-08-11: Trusted Seed recruitment & Hatch prep 210 backers onboarded via Commons Stack Hatch app 2021-08-11: Hatch closed — 1,571,224 wxDAI raised from 210 backers #hatch https://medium.com/token-engineering-commons/the-hatch-was-successful-2443cd07cbb0 2021-08-12 / 2022-01-26: Commons Configuration Dashboard — 40 parameter proposals, 2 rounds of voting #hatch endSection section ABC & Markets #abc 2022-01-25: 500K whale purchase — 499,803 xDAI (46.5% of all ABC buy volume, ever) #abc wallet 0xf5dcd98a...12f35497 · entry tribute ~109,957 xDAI 2022-01-27: ABC live on Gnosis Chain; first CV proposal submitted #abc #cv 2022-05-09: Terra/Luna collapse — first exogenous shock #shock 2022-11-08: FTX collapse — second exogenous shock, crypto winter deepens #shock 2023-12-01: ABC price bottom — 0.36 xDAI (-88% from peak of 2.94) #abc endSection section Treasury & Migration #migration 2023-12-15: Migration to Optimism; CV replaced by multisig coordination #migration reserve currency: xDAI → rETH https://forum.tecommons.org/t/and-we-are-live-on-op-mainnet/1324 2024-01-01 / 2025-11-04: Post-migration draw-down era (manual allocations) 2025-04-01: Q1 2025 Progress Report — tribute revenue $3,614/qtr vs $42,244/qtr opex #shutdown https://forum.tecommons.org/t/tec-q1-2025-progress-report/1472 2025-11-04: TEC shutdown proposal formally approved #shutdown https://forum.tecommons.org/t/tec-shutdown-proposal/1520 endSection """ def build_cv_section(rows: list[dict], cats: dict[str, str]) -> str: lines = ["section Conviction Voting Proposals #cv", ""] for row in rows: pid = row["id"] try: block = int(row["block"]) except (TypeError, ValueError): continue created = block_to_date(block) executed = None if row.get("executed_block"): try: executed = block_to_date(int(row["executed_block"])) except ValueError: pass amount = row.get("amount_requested") or "0" try: amount_f = float(amount) except ValueError: amount_f = 0.0 status = (row.get("status") or "").lower() title = (row.get("link") or f"Proposal #{pid}").strip() or f"Proposal #{pid}" title = title.replace("\n", " ")[:90] cat = cats.get(str(pid), "") tags = [f"#{status}"] if status else [] if cat: tags.append(f"#{cat.lower().replace(' ', '_')}") tag_str = " " + " ".join(tags) if tags else "" if executed and status == "executed": rng = f"{created:%Y-%m-%d} / {executed:%Y-%m-%d}" else: rng = f"{created:%Y-%m-%d}" amount_note = f" — {amount_f:,.0f} TEC" if amount_f > 0 else "" lines.append(f"{rng}: #{pid} {title}{amount_note}{tag_str}") lines.append("endSection") return "\n".join(lines) + "\n" def main() -> None: rows = list(csv.DictReader(PROPOSALS.open())) cats = load_categories() body = HEADER + build_cv_section(rows, cats) OUT.parent.mkdir(exist_ok=True) OUT.write_text(body) print(f"Wrote {OUT} ({len(rows)} proposals)") if __name__ == "__main__": main()