From e76929039f8d26c510f9341861a70165aaed9e5a Mon Sep 17 00:00:00 2001 From: hanzo-dev Date: Fri, 10 Jul 2026 14:38:04 -0700 Subject: [PATCH] fix(captable): exclude terminal-state options from fully-diluted math (RED M5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit capTable summed ALL option rows into optionShares / per-stakeholder options regardless of status, so EXERCISED, EXPIRED and CANCELLED grants inflated fullyDilutedShares and understated every real ownership %. A cap-table surface must not do this. Filter both dilution queries (the totals sum and the per-stakeholder subquery) to OUTSTANDING options only: status NOT IN ('EXERCISED','EXPIRED','CANCELLED'). EXERCISED options already became shares (counted in `share`) — counting the option too double-counts; EXPIRED/CANCELLED are void. DRAFT + ACTIVE still dilute. Rebuilt goja/bundle.js. Existing goja bundle tests pass. Claude-Session: https://claude.ai/code/session_016yg7GPhYdWCh9vpp4HEwLZ --- goja/bundle.js | 8 ++++++-- goja/src/routes/captable.ts | 19 ++++++++++++++++--- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/goja/bundle.js b/goja/bundle.js index 745a51c1..8ad033b7 100644 --- a/goja/bundle.js +++ b/goja/bundle.js @@ -1160,6 +1160,7 @@ } // src/routes/captable.ts + var DILUTIVE_OPTION_STATUS = `status NOT IN ('EXERCISED','EXPIRED','CANCELLED')`; function capTable(ctx) { const cid = ctx.companyId; const company = db.query(`SELECT id, name FROM company WHERE id = ?`, [cid]); @@ -1170,14 +1171,17 @@ "n" ); const optionShares = num2( - db.query(`SELECT COALESCE(SUM(quantity),0) AS n FROM option WHERE company_id = ?`, [cid]), + db.query( + `SELECT COALESCE(SUM(quantity),0) AS n FROM option WHERE company_id = ? AND ${DILUTIVE_OPTION_STATUS}`, + [cid] + ), "n" ); const fullyDiluted = outstanding + optionShares; const byStakeholder = db.query( `SELECT st.id AS stakeholderId, st.name, COALESCE((SELECT SUM(sh.quantity) FROM share sh WHERE sh.stakeholder_id = st.id AND sh.company_id = st.company_id), 0) AS shares, - COALESCE((SELECT SUM(op.quantity) FROM option op WHERE op.stakeholder_id = st.id AND op.company_id = st.company_id), 0) AS options + COALESCE((SELECT SUM(op.quantity) FROM option op WHERE op.stakeholder_id = st.id AND op.company_id = st.company_id AND op.${DILUTIVE_OPTION_STATUS}), 0) AS options FROM stakeholder st WHERE st.company_id = ? ORDER BY shares DESC, options DESC`, diff --git a/goja/src/routes/captable.ts b/goja/src/routes/captable.ts index e9885318..e96eef2a 100644 --- a/goja/src/routes/captable.ts +++ b/goja/src/routes/captable.ts @@ -1,7 +1,7 @@ // Cap-table computation — the read the whole pilot exists to prove round-trips // through Base. A real aggregation over issued equity: // - outstanding = Σ share.quantity -// - fullyDiluted = outstanding + Σ option.quantity (granted options) +// - fullyDiluted = outstanding + Σ OUTSTANDING option.quantity // - per-stakeholder ownership on a fully-diluted basis // - per-share-class authorized vs issued // - convertibles (SAFE + note capital not yet converted) as a side section @@ -10,6 +10,16 @@ import { db } from "../host"; import { type Ctx, notFound, okRes, type Res } from "./common"; +// Only OUTSTANDING options dilute the cap table. Options in a TERMINAL state are +// excluded from the fully-diluted math and every ownership %: +// - EXERCISED → already converted to shares (counted in `share`); counting the +// option too would DOUBLE-COUNT the same equity. +// - EXPIRED / CANCELLED → void; the shares will never be issued. +// DRAFT + ACTIVE remain (granted / pending grants that still dilute). Summing ALL +// option rows regardless of status inflated fullyDilutedShares and understated +// every stakeholder's true ownership — a correctness bug on a cap-table surface. +const DILUTIVE_OPTION_STATUS = `status NOT IN ('EXERCISED','EXPIRED','CANCELLED')`; + export function capTable(ctx: Ctx): Res { const cid = ctx.companyId; const company = db.query(`SELECT id, name FROM company WHERE id = ?`, [cid]); @@ -23,7 +33,10 @@ export function capTable(ctx: Ctx): Res { "n", ); const optionShares = num( - db.query(`SELECT COALESCE(SUM(quantity),0) AS n FROM option WHERE company_id = ?`, [cid]), + db.query( + `SELECT COALESCE(SUM(quantity),0) AS n FROM option WHERE company_id = ? AND ${DILUTIVE_OPTION_STATUS}`, + [cid], + ), "n", ); const fullyDiluted = outstanding + optionShares; @@ -32,7 +45,7 @@ export function capTable(ctx: Ctx): Res { const byStakeholder = db.query( `SELECT st.id AS stakeholderId, st.name, COALESCE((SELECT SUM(sh.quantity) FROM share sh WHERE sh.stakeholder_id = st.id AND sh.company_id = st.company_id), 0) AS shares, - COALESCE((SELECT SUM(op.quantity) FROM option op WHERE op.stakeholder_id = st.id AND op.company_id = st.company_id), 0) AS options + COALESCE((SELECT SUM(op.quantity) FROM option op WHERE op.stakeholder_id = st.id AND op.company_id = st.company_id AND op.${DILUTIVE_OPTION_STATUS}), 0) AS options FROM stakeholder st WHERE st.company_id = ? ORDER BY shares DESC, options DESC`,