TEC-analysis/scripts/gen_timeline.py

139 lines
4.7 KiB
Python

"""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()