fix(captable): exclude terminal-state options from fully-diluted math (RED M5) (#3)
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
Co-authored-by: hanzo-dev <dev@hanzo.ai>
This commit is contained in:
+6
-2
@@ -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`,
|
||||
|
||||
@@ -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`,
|
||||
|
||||
Reference in New Issue
Block a user