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:
z
2026-07-10 14:39:06 -07:00
committed by GitHub
co-authored by hanzo-dev
parent 3cc625ee44
commit 119bf9c088
2 changed files with 22 additions and 5 deletions
+6 -2
View File
@@ -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`,
+16 -3
View File
@@ -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`,