diff --git a/backend/server.py b/backend/server.py index d30e37a..e2b2266 100644 --- a/backend/server.py +++ b/backend/server.py @@ -469,6 +469,13 @@ def init_db(): except Exception as _e: print(f"[backfill] grid contact_id backfill warning: {_e}") + # One-time: seed the v5 thesis into the Architect's Workshop if it is empty. + try: + from thesis_seed import ensure_thesis_seed as _ensure_thesis_seed + _ensure_thesis_seed(conn) + except Exception as _e: + print(f"[thesis] seed warning: {_e}") + conn.close() print(f"Database initialized at {DB_PATH}") diff --git a/backend/test_thesis_seed.py b/backend/test_thesis_seed.py new file mode 100644 index 0000000..72a6ce1 --- /dev/null +++ b/backend/test_thesis_seed.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +"""Offline test for the v5 thesis seed (init_db auto-seed + idempotency). + +Runs the real init_db against a throwaway DB (applies migration 0002 and the +auto-seed), then asserts the Workshop substrate: a core line, one line per segment, +the Option A/B banner as a 2-member variant group, the pillars/proof, and the +segments table — and that re-seeding is a no-op. + +Run: cd backend && python3 test_thesis_seed.py +""" +import os +import sqlite3 +import sys +import tempfile + +_tmp = tempfile.mkdtemp() +os.environ["CRM_DATA_DIR"] = _tmp +os.environ["CRM_DB_PATH"] = os.path.join(_tmp, "crm.db") +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +import server # noqa: E402 +import thesis_seed # noqa: E402 + +FAILS = [] + + +def check(cond, msg): + print((" PASS " if cond else " FAIL ") + msg) + if not cond: + FAILS.append(msg) + + +def main(): + server.init_db() + conn = sqlite3.connect(server.DB_PATH) + conn.row_factory = sqlite3.Row + + lines = {r["line_key"]: dict(r) for r in conn.execute("SELECT * FROM thesis_lines WHERE deleted_at IS NULL")} + check("core" in lines and lines["core"]["is_core"] == 1, "core thesis line seeded (is_core)") + seg_lines = [k for k in lines if k.startswith("seg_")] + check(len(seg_lines) == 5, f"five per-segment lines seeded (got {len(seg_lines)}: {sorted(seg_lines)})") + check(len(lines) == 6, f"exactly 6 lines total (got {len(lines)})") + + core_id = lines["core"]["id"] + nodes = [dict(r) for r in conn.execute("SELECT * FROM thesis_nodes WHERE line_id=?", (core_id,))] + types = [n["node_type"] for n in nodes] + check("throughline" in types, "core has a throughline node") + check("proof_point" in types, "core has a proof_point node") + variants = [n for n in nodes if n["variant_group"] == "positioning"] + check(len(variants) == 2, f"Option A/B banner is a 2-member variant group (got {len(variants)})") + pillars = [n for n in nodes if n["node_type"] == "claim" and n["title"] and n["title"][0] in "123"] + check(len(pillars) == 3, f"three pillar claims (got {len(pillars)})") + + segs = {r["segment_key"] for r in conn.execute("SELECT segment_key FROM segments WHERE status='active'")} + check(segs == {"btc_native_hnwi", "institution", "family_office", "smaller_accredited", "ai_energy_operator"}, + f"five active segments (got {sorted(segs)})") + + # segment lines each carry a segment_cut angle + cut = conn.execute("""SELECT COUNT(*) FROM thesis_nodes n JOIN thesis_lines l ON l.id=n.line_id + WHERE n.node_type='segment_cut' AND l.line_key LIKE 'seg_%'""").fetchone()[0] + check(cut == 5, f"each segment line has a segment_cut angle (got {cut})") + + # idempotency: re-seeding does nothing (thesis not empty) + thesis_seed.ensure_thesis_seed(conn) + conn.commit() + n2 = conn.execute("SELECT COUNT(*) FROM thesis_lines WHERE deleted_at IS NULL").fetchone()[0] + check(n2 == 6, f"re-seed is a no-op when thesis already exists (got {n2})") + + conn.close() + print() + if FAILS: + print(f"FAILED ({len(FAILS)})") + sys.exit(1) + print("ALL PASS (v5 thesis seed)") + + +if __name__ == "__main__": + main() diff --git a/backend/thesis_seed.py b/backend/thesis_seed.py new file mode 100644 index 0000000..dc3aed2 --- /dev/null +++ b/backend/thesis_seed.py @@ -0,0 +1,202 @@ +"""Seed the Ten31 v5 thesis into the Architect's substrate (Phase 1). + +Builds the starting "living messaging source of truth" the partners iterate on in +the Thesis Workshop: a CORE line (throughline, the open Option A/B banner debate as +competing variants, the three pillars, the proof, and the voice rules) plus one +LINE PER SEGMENT carrying that segment's angle, and the segment definitions. + +Content is Ten31's OWN messaging (docs/thesis-seed-v5.md) — not LP data — so it is +safe to ship in code. Everything lands as DRAFT/CANDIDATE nodes: nothing is made +canonical here (that is the partners' dual-approval action — guardrail #4). + +`ensure_thesis_seed(conn)` is idempotent: it seeds ONLY when no thesis lines exist, +so it bootstraps an empty Workshop once and never clobbers partner edits afterward. +""" +import json +import sqlite3 +import uuid +from datetime import datetime, timezone + + +def _now(): + return datetime.now(timezone.utc).replace(tzinfo=None).isoformat() + "Z" + + +def _eid(prefix): + return f"{prefix}_{uuid.uuid4().hex[:16]}" + + +def _log(conn, action, target_id, payload): + conn.execute( + """INSERT INTO interaction_log + (id, ts, actor_type, actor_id, action, target_type, target_id, payload, source, created_at) + VALUES (?,?, 'system', 'thesis_seed', ?, 'thesis', ?, ?, 'seed', ?)""", + (str(uuid.uuid4()), _now(), action, target_id, json.dumps(payload) if payload is not None else None, _now()), + ) + + +def _line(conn, line_key, name, segment_key=None, is_core=False, description=None): + lid = _eid("thl") + conn.execute( + """INSERT INTO thesis_lines (id, line_key, name, segment_key, is_core, description, created_at, updated_at) + VALUES (?,?,?,?,?,?,?,?)""", + (lid, line_key, name, segment_key, 1 if is_core else 0, description, _now(), _now()), + ) + _log(conn, "thesis.line_seeded", lid, {"line_key": line_key, "is_core": bool(is_core)}) + return lid + + +def _node(conn, line_id, parent_id, node_type, ordn, title, body, status="draft", variant_group=None): + nid = _eid("thn") + conn.execute( + """INSERT INTO thesis_nodes (id, line_id, parent_id, node_type, ord, title, body, status, variant_group, created_at, updated_at) + VALUES (?,?,?,?,?,?,?,?,?,?,?)""", + (nid, line_id, parent_id, node_type, float(ordn), title, body, status, variant_group, _now(), _now()), + ) + return nid + + +# ── v5 content (docs/thesis-seed-v5.md) ────────────────────────────────────── + +THROUGHLINE = ( + "Bitcoin, AI, and energy are three of the largest growth markets of the next decade, " + "and they depend on the same scarce resources: cheap energy and computing power. We " + "believe that energy, compute, and AI infrastructure will settle on money that is hard " + "to produce. That is not the case today, and connecting these markets to bitcoin is the " + "part of the thesis that very few others are making, even as broader crypto tries to " + "attach itself to AI and energy. Ten31 invests in that infrastructure with strong conviction." +) + +OPTION_A = ( + "Ten31 invests in the infrastructure of scarcity. We back the bitcoin, energy, and AI " + "companies that produce and secure the scarce resources these markets are built on." +) +OPTION_B = ( + "Ten31 invests in freedom technology. We back the bitcoin, energy, and AI companies " + "building the foundation for a more sovereign, less centralized economy." +) + +PILLAR_1 = ( + "Every one of these markets is bottlenecked on something scarce. AI and bitcoin both " + "compete for cheap energy and compute. And we believe energy, compute, and AI " + "infrastructure will increasingly settle on money that is hard to produce, which points " + "directly at bitcoin. The companies that own and supply the scarce side of that equation " + "capture the value as demand grows. That is where we invest. (The bitcoin connection is a " + "forward-looking conviction, not a description of today. That gap is the opportunity.)" +) +PILLAR_2 = ( + "We invest in foundational infrastructure with real revenue: the companies that generate " + "energy, secure capital, and power computation. Real businesses earning real money from " + "real demand today." +) +PILLAR_3 = ( + "Founders come to us because of our experience and our genuine alignment with bitcoin. We " + "pursue and lead opportunities exclusive to us. People in this ecosystem know our track " + "record and want us on their side." +) + +PROOF = ( + "$200M+ deployed across two funds into 30+ of the strongest companies in the space " + "(Strike, Start9, energy and mining infrastructure). A six-year track record that includes " + "large-scale M&A and public-markets activity unmatched by others in this space. Fund III " + "continues the same strategy." +) + +VOICE = ( + "Direct, concrete, conviction-driven. No \"betting\" language, no em dashes, no \"X, not Y\" " + "phrasing, no kitchen-sink lists. Plain sentences a serious LP can verify in their head." +) + +POSITIONING_NOTE = ( + "Open partner decision: which banner do we lead with? Option A frames Ten31 around " + "scarcity; Option B frames it around freedom technology (a banner in the spirit of a16z's " + "\"American Dynamism\"). Both wordings are still being refined — use the Architect to draft " + "more options." +) + +# segment_key -> (display name, definition, needs_to_hear angle) +SEGMENTS = [ + ("btc_native_hnwi", "Bitcoin-native HNWIs (OGs)", + "Long-time bitcoin holders with significant net worth who care about the ecosystem succeeding.", + "Bitcoin only wins if people build on it. Holding is not enough. You care about making " + "bitcoin succeed, and so do we. We put capital behind the companies that turn bitcoin into " + "a working economy.", + "Don't lecture OGs or imply they don't understand bitcoin; don't suggest that simply holding is the goal."), + ("institution", "Institutions", + "Institutional allocators evaluating exposure to the bitcoin, energy, and AI buildout.", + "Exposure to the bitcoin, energy, and AI buildout through a team with a six-year " + "institutional track record, including large-scale M&A and public-markets activity unmatched " + "by others in this space (and Grant's prior institutional experience on top of that).", + "No hype or moonshot framing. Lead with the verifiable track record, not vision."), + ("family_office", "Family offices", + "Multi-generational capital seeking durable, long-horizon allocations.", + "A long-horizon allocation grounded in real businesses, run by a team with deep credibility " + "and a real track record.", + "Avoid short-term or trader framing; emphasize durability, real businesses, and horizon."), + ("smaller_accredited", "Smaller accredited ($100k)", + "Accredited investors entering at a more accessible commitment size.", + "The same thesis our most convicted investors back, at an accessible entry point.", + "Don't talk down to them; it is the same thesis, just an accessible entry point."), + ("ai_energy_operator", "AI & energy operators", + "Operators in AI and energy who are not yet focused on bitcoin.", + "You may not be focused on bitcoin today, and that is exactly the point. We believe bitcoin " + "becomes a larger component of energy and compute over time, and most operators in your " + "space are not yet positioned for it. We are, and we invest across the stack that connects them.", + "Don't assume they're bitcoin-focused and don't preach; connect bitcoin as a growing " + "component of their world over time."), +] + + +def seed_v5(conn): + """Insert the full v5 thesis (core line + per-segment lines + segments). Assumes + the caller has confirmed the thesis is empty; uses fresh ids.""" + # ── segments table ── + for key, name, definition, needs, avoid in SEGMENTS: + sid = _eid("seg") + conn.execute( + """INSERT INTO segments (id, segment_key, name, definition, needs_to_hear, avoid, version_no, status, created_at, updated_at) + VALUES (?,?,?,?,?,?,1,'active',?,?)""", + (sid, key, name, definition, needs, avoid, _now(), _now()), + ) + + # ── core line ── + core = _line(conn, "core", "Core Thesis", is_core=True, + description="The shared spine of the Ten31 thesis — throughline, banner, pillars, and proof.") + root = _node(conn, core, None, "thesis_root", 0, "Ten31 — Core Thesis", "") + _node(conn, core, root, "throughline", 1, "Throughline", THROUGHLINE) + + pos = _node(conn, core, root, "section", 2, "Positioning / Banner (open debate)", POSITIONING_NOTE) + _node(conn, core, pos, "claim", 1, "Option A — Scarcity-forward", OPTION_A, status="candidate", variant_group="positioning") + _node(conn, core, pos, "claim", 2, "Option B — Freedom tech as the banner", OPTION_B, status="candidate", variant_group="positioning") + + pillars = _node(conn, core, root, "section", 3, "Pillars", "") + _node(conn, core, pillars, "claim", 1, "1. Scarcity is the whole opportunity", PILLAR_1) + _node(conn, core, pillars, "claim", 2, "2. Foundational infrastructure with real revenue", PILLAR_2) + _node(conn, core, pillars, "claim", 3, "3. Founders seek us out, and we lead deals others never see", PILLAR_3) + + _node(conn, core, root, "proof_point", 4, "The proof", PROOF) + _node(conn, core, root, "section", 5, "Voice", VOICE) + + # ── per-segment lines ── + for key, name, _definition, needs, _avoid in SEGMENTS: + lid = _line(conn, f"seg_{key}", name, segment_key=key, is_core=False, + description=f"Segment-specific angle for {name}.") + sroot = _node(conn, lid, None, "thesis_root", 0, name, "") + _node(conn, lid, sroot, "segment_cut", 1, "Angle", needs) + + return {"core_line": core, "segments": len(SEGMENTS)} + + +def ensure_thesis_seed(conn): + """Seed the v5 thesis once, only when the Workshop is empty (no thesis lines). + Idempotent and non-destructive: never overwrites partner edits.""" + try: + n = conn.execute("SELECT COUNT(*) FROM thesis_lines WHERE deleted_at IS NULL").fetchone()[0] + except sqlite3.OperationalError: + return # thesis tables not present yet (migration 0002 not applied) + if n: + return + out = seed_v5(conn) + _log(conn, "thesis.seeded_v5", out["core_line"], {"segments": out["segments"], "source": "thesis-seed-v5"}) + conn.commit() + print(f"[thesis] seeded v5 thesis (core line + {out['segments']} segment lines)") diff --git a/start9/0.4/startos/utils.ts b/start9/0.4/startos/utils.ts index 97d0ead..bc2ae0d 100644 --- a/start9/0.4/startos/utils.ts +++ b/start9/0.4/startos/utils.ts @@ -17,8 +17,9 @@ export const PACKAGE_TITLE = 'Ten31 Database' // * 0.1.0:49 (Architect: Claude thesis generation + Thesis Workshop screen) // * 0.1.0:50 (Set Anthropic API Key UI action — no terminal needed) // * 0.1.0:51 (entity-resolution fix: people double-count + duplicate queue) -// * Current: 0.1.0:52 (grid/contacts unification: contact_id link + grid as front door) -export const PACKAGE_VERSION = '0.1.0:52' +// * 0.1.0:52 (grid/contacts unification: contact_id link + grid as front door) +// * Current: 0.1.0:53 (seed v5 thesis into the Architect Workshop) +export const PACKAGE_VERSION = '0.1.0:53' export const DATA_MOUNT_PATH = '/data' export const WEB_PORT = 8080 diff --git a/start9/0.4/startos/versions/index.ts b/start9/0.4/startos/versions/index.ts index dd81fda..ef90d35 100644 --- a/start9/0.4/startos/versions/index.ts +++ b/start9/0.4/startos/versions/index.ts @@ -13,8 +13,9 @@ import { v_0_1_0_49 } from './v0.1.0.49' import { v_0_1_0_50 } from './v0.1.0.50' import { v_0_1_0_51 } from './v0.1.0.51' import { v_0_1_0_52 } from './v0.1.0.52' +import { v_0_1_0_53 } from './v0.1.0.53' export const versionGraph = VersionGraph.of({ - current: v_0_1_0_52, - other: [v_0_1_0_39, v_0_1_0_40, v_0_1_0_41, v_0_1_0_42, v_0_1_0_43, v_0_1_0_44, v_0_1_0_45, v_0_1_0_46, v_0_1_0_47, v_0_1_0_48, v_0_1_0_49, v_0_1_0_50, v_0_1_0_51], + current: v_0_1_0_53, + other: [v_0_1_0_39, v_0_1_0_40, v_0_1_0_41, v_0_1_0_42, v_0_1_0_43, v_0_1_0_44, v_0_1_0_45, v_0_1_0_46, v_0_1_0_47, v_0_1_0_48, v_0_1_0_49, v_0_1_0_50, v_0_1_0_51, v_0_1_0_52], }) diff --git a/start9/0.4/startos/versions/v0.1.0.53.ts b/start9/0.4/startos/versions/v0.1.0.53.ts new file mode 100644 index 0000000..2cc1bd1 --- /dev/null +++ b/start9/0.4/startos/versions/v0.1.0.53.ts @@ -0,0 +1,21 @@ +import { VersionInfo } from '@start9labs/start-sdk' + +// Seed the v5 thesis into the Architect's Thesis Workshop. On upgrade, if the +// Workshop is empty, it is populated once with the core line (throughline, the +// open Option A/B banner debate as competing variants, the three pillars, the +// proof, voice rules), one line per LP segment carrying that segment's angle, and +// the segment definitions. Everything lands as draft/candidate — nothing is made +// canonical (that stays the partners' dual-approval action). Idempotent: never +// overwrites partner edits. No schema migration. +export const v_0_1_0_53 = VersionInfo.of({ + version: '0.1.0:53', + releaseNotes: { + en_US: [ + 'Seeds the v5 thesis into the Thesis Workshop so you and your partner can start', + 'iterating immediately: the core line (throughline, the Option A/B banner as', + 'competing options, three pillars, the proof) plus a line per LP segment with its', + 'angle. Seeds once when the Workshop is empty and never overwrites your edits.', + ].join(' '), + }, + migrations: { up: async () => {}, down: async () => {} }, +})