Skip to content

Commit 4ed55e6

Browse files
committed
Full refactor
Update from just the validator to OCF Tools and refactor as an NPM package. Also updated validator from xState v4 to v5.
1 parent 9283fb6 commit 4ed55e6

File tree

296 files changed

+7961
-27738
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

296 files changed

+7961
-27738
lines changed

.editorconfig

-8
This file was deleted.

.eslintignore

-1
This file was deleted.

.eslintrc.json

-6
This file was deleted.

.prettierrc.js

-3
This file was deleted.

LICENSE.md

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
### Schema and Documentation License
2+
3+
Version 1.0.1, April 3, 2024
4+
5+
Copyright (C) 2024 Open Cap Table Coalition <https://opencaptablecoalition.com/>
6+
7+
#### 1. LICENSE
8+
9+
**By using and/or copying Open Cap Table Coalition JSON Schema files or documentation thereof
10+
(each, individually, an “OCF File”, and, collectively, the “OCF Files”), you (the licensee)
11+
agree that you have read, understood, and will comply with the following terms and conditions**:
12+
13+
Permission to copy and distribute OCF Files, in any medium for any purpose and without fee or
14+
royalty is hereby granted, provided that
15+
16+
1. For _ALL_ copies of an OCF File or portions thereof that are **not** JSON Schema files, you include
17+
18+
- A link to the original OCF File and
19+
- A notice in the form "Copyright © [$date-of-ocf-file] [Open Cap Table Coalition](https://opencaptablecoalition.com)."; and
20+
21+
2. For _ALL_ copies of an OCF File or portions thereof that **are** JSON Schema files, you include
22+
23+
- A `$comment` field with a value of: "Copyright © [$date-of-ocf-file]
24+
Open Cap Table Coalition (https://opencaptablecoalition.com) / Original File: [$url-of-original-schema-file]"
25+
26+
When space permits, inclusion of the full text of the **NOTICE** (as defined below) should be provided. We request
27+
that authorship attribution be provided in any software, documents, or other items or products
28+
that you create pursuant to the implementation of OCF Files, or any portion thereof.
29+
30+
No right to create modifications or derivatives of OCF Files is granted pursuant to this license,
31+
except as follows:
32+
33+
_To facilitate implementation of the technical specifications set forth in
34+
the OCF Files, anyone may prepare and distribute derivative works and
35+
portions of the OCF Files in software, in supporting materials accompanying
36+
software, and in documentation of software, PROVIDED that all such
37+
works include the notice below. HOWEVER, the publication of derivative
38+
works of OCF Files for use as a technical specification is expressly prohibited._
39+
40+
In addition, "Code Components" — sample OCF file implementations, sample OCF JSONs and computer
41+
programming language code — are licensed under the [Apache 2 License](https://www.apache.org/licenses/LICENSE-2.0).
42+
43+
The full "Notice" is:
44+
45+
> "Copyright © [$date-of-ocf-file] Open Cap Table Coalition. This software or document
46+
> includes material copied from or derived from [title and URI of the OCF File]."
47+
48+
#### 2. DISCLAIMERS
49+
50+
OCF FILES ARE PROVIDED "AS IS," AND COPYRIGHT HOLDERS MAKE NO REPRESENTATIONS OR WARRANTIES,
51+
EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY, FITNESS
52+
FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, OR TITLE; THAT THE CONTENTS OF THE OCF FILES ARE
53+
SUITABLE FOR ANY PURPOSE; NOR THAT THE IMPLEMENTATION OF SUCH CONTENTS WILL NOT INFRINGE ANY
54+
THIRD PARTY PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER RIGHTS. COPYRIGHT HOLDERS WILL NOT BE LIABLE
55+
FOR ANY DIRECT, INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE OF THE OCF FILES
56+
OR THE PERFORMANCE OR IMPLEMENTATION OF THE CONTENTS THEREOF.
57+
58+
The name and trademarks of copyright holders may NOT be used in advertising or publicity pertaining
59+
to the OCF Files or their contents without specific, written prior permission. Title to copyright
60+
in the OCF Files will at all times remain with copyright holders.

README.md

+22-10
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,30 @@
1-
# This is the main repository for the OCF-XState-Validation project.
1+
Open Cap Table Format (OCF) Toolset
22

3-
Run `npm install` to install dependencies.
3+
Version: 0.1.0
44

5-
This project is a work-in-progress.
5+
Date: 21 October 2024
66

7-
The goal of this project is to create the tooling to logically and structually validate an OCF package using xstate. A side-product of this process is that the xstate machince will hopefully be able to create end-of-day (EOD) snapshots from an OCF package.
7+
This toolset provides a growing toolset to help validate and utilize Open Cap Table Format datasets.
88

9-
To run the project, you can use the command `npm start` to run `src/index.ts`.
9+
Currently, the dataset includes 6 tools:
1010

11-
To run the validator for a specific manifest run the command with the path to the manifest as an argument. Example:
12-
`npm start ./src/test_data/company_valid/Manifest.ocf.json`
11+
1. Read OCF Package: This tool creates a workable JSON object of the content of an OCF folder from the path of the directory holding the OCF files.
12+
`constocfPackage = readOcfPackage(ocfPackageFolderDir);`
13+
2. Vesting Schedule Generator: This tool creates a JSON array of the vesting periods for a "time standard" (i.e. a schedule in the form of " 4 years monthly" periods ). The tool also calculates and shows exercise transactions for the equity compensation issuance. The tool can handle any type of allocation type and any upfront vesting or cliff periods. (NOTE: This tool utilizes the concept of a cliff_condition inside the relative time vesting condition which is not in the current released version of OCF as of Oct 21, 2024).
14+
`constschedule = generateSchedule(ocfPackageFolderDir, equityCompensationIssuanceSecurityId);`
15+
3. Vesting Status Check: Building on the Vesting Schedule Generator, this tool allows a user to find the exact status of unvested, vested, and exercise options at a given time.
16+
`conststatus = vestingStatusCheck(ocfPackageFolderDir,equityCompensationIssuanceSecurityId, vestingStatusDate);`
17+
4. ISO / NSO Split Calculator: This tool allows a user to determine the ISO / NSO split for equity compensation issuances for a given stakeholder. This tool shows the split of NSO/ISO for each vesting period of the relative equity compensation issuances. This tool uses the vesting schedule generator under the hood and will only work for vesting schedules that can be generated using that tool.
18+
`constisoNso = isoNsoCalculator(ocfPackageFolderDir, isoCheckStakeholder, isoCapacity);`
19+
5. OCF Validator: This tool tests the logical and structural validity of an OCF package. We are continuing to build out the rules set for validity but have good coverage for stock transactions and basic validations for all other transactions. The tool outputs a JSON object with the varibles of `result: string` , `report: string[]` and `snapshots: any[]` . The result shows if the package is valid or what the issue is if it is not. The report shows a list of all the validity checks completed per transaction and snapshots shows an array of end of day captables based on the package.
20+
`constocfValidation = ocfValidator(ocfPackageFolderDir);`
21+
6. OCF Snapshot: This tool allows the user to see the outstanding captable of a OCF package on a given date.
22+
`constsnapshot = ocfSnapshot(ocfPackageFolderDir, ocfSnapshotDate);`
1323

14-
The system takes the desired ocf package (located in `src/test_data`) and runs it through an xstate machine (located at `src/ocfMachine.ts`) and applies the validators for OCF transactions located in the `src/validators` folder.
24+
Usage: (before publication to NPM)
1525

16-
As of Oct 17, 2023, the ocfMachine only checks stock related transactions and the validators for 'tx_stock_issuance', 'tx_stock_transfer', and 'tx_stock_cancellation' have been partially implemented. (All other validators are just placeholder code).
26+
Download this repository and run `npm i; npm run build; npm link;`
1727

18-
Work needs to be done to update each individual validator function and to add the non-stock related transactions to the state machine. Additionally, more robust test_data could be developed to include all transaction types.
28+
In the project that you want to use ocf-tools, run `npm link ocf-tools` and add
29+
` const { readOcfPackage, generateSchedule, vestingStatusCheck, isoNsoCalculator, ocfValidator, ocfSnapshot } = require("ocf-tools");`
30+
to the top of the file you want to use the ocf-tools in.

dist/index.js

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"use strict";
2+
Object.defineProperty(exports, "__esModule", { value: true });
3+
const read_ocf_package_1 = require("./read_ocf_package");
4+
const vesting_schedule_generator_1 = require("./vesting_schedule_generator");
5+
const vesting_status_check_1 = require("./vesting_status_check");
6+
const iso_nso_calculator_1 = require("./iso_nso_calculator");
7+
const ocf_validator_1 = require("./ocf_validator");
8+
const ocf_snapshot_1 = require("./ocf_snapshot");
9+
module.exports = { readOcfPackage: read_ocf_package_1.readOcfPackage, generateSchedule: vesting_schedule_generator_1.generateSchedule, vestingStatusCheck: vesting_status_check_1.vestingStatusCheck, isoNsoCalculator: iso_nso_calculator_1.isoNsoCalculator, ocfValidator: ocf_validator_1.ocfValidator, ocfSnapshot: ocf_snapshot_1.ocfSnapshot };

dist/iso_nso_calculator/index.js

+139
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
"use strict";
2+
Object.defineProperty(exports, "__esModule", { value: true });
3+
exports.isoNsoCalculator = void 0;
4+
const read_ocf_package_1 = require("../read_ocf_package");
5+
const vesting_schedule_generator_1 = require("../vesting_schedule_generator");
6+
const addYearAndGrantId = (vestingSchedule, issuanceId) => {
7+
return vestingSchedule.map((entry) => {
8+
const date = new Date(entry.Date);
9+
return Object.assign(Object.assign({}, entry), { Grant: issuanceId, Year: date.getFullYear() });
10+
});
11+
};
12+
// Function to sum the amounts by year
13+
const sumByYear = (vestingSchedule) => {
14+
// Create an empty object to store the sums by year
15+
const result = {};
16+
// Iterate through the array
17+
vestingSchedule.forEach((entry) => {
18+
if (entry["Event Type"] !== "Exercise") {
19+
const year = entry.Year;
20+
const amount = entry["Event Quantity"];
21+
// If the year is already in the result, add the amount
22+
if (result[year]) {
23+
result[year] += amount;
24+
}
25+
else {
26+
// Otherwise, set the initial amount for the year
27+
result[year] = amount;
28+
}
29+
}
30+
});
31+
// Convert the result object into an array of objects for displaying
32+
const resultTable = Object.keys(result).map((yearString) => {
33+
const year = parseInt(yearString, 10); // Convert the key back to a number
34+
return {
35+
Grant: vestingSchedule[0].Grant,
36+
Year: year,
37+
TotalAmountVested: result[year],
38+
FMV: 0,
39+
VestedValue: 0,
40+
};
41+
});
42+
return resultTable;
43+
};
44+
// Function to calculate capacity
45+
function calculateCapacity(vestingData, capacityPerYear) {
46+
const result = [];
47+
let remainingCapacity = capacityPerYear;
48+
let currentYear = vestingData[0].Year; // Initialize the current year to the year of the first row
49+
vestingData.forEach((row) => {
50+
// If the year changes, reset the remaining capacity to the full capacity for the new year
51+
if (row.Year !== currentYear) {
52+
remainingCapacity = capacityPerYear;
53+
currentYear = row.Year; // Update the current year
54+
}
55+
// Determine how much capacity is used in this row
56+
const usedCapacity = Math.min(row.VestedValue, remainingCapacity);
57+
// Update remaining capacity after usage
58+
remainingCapacity -= usedCapacity;
59+
// Add Capacity Used and Capacity Remaining to the row
60+
result.push(Object.assign(Object.assign({}, row), { CapacityUsed: usedCapacity, CapacityRemaining: remainingCapacity }));
61+
});
62+
return result;
63+
}
64+
// Function to add ISO Shares and NSO Shares to the vesting data
65+
function addSharesColumns(vestingData) {
66+
return vestingData.map((row) => {
67+
const isoShares = Math.round(row.CapacityUsed / row.FMV);
68+
const nsoShares = Math.round((row.VestedValue - row.CapacityUsed) / row.FMV);
69+
return Object.assign(Object.assign({}, row), { ISOShares: isoShares, NSOShares: nsoShares });
70+
});
71+
}
72+
// Function to add ISO Used, ISO Remaining, and NSO columns
73+
function addISOColumns(vestingData, isoData) {
74+
const result = [];
75+
const isoRemainingByGrantAndYear = {};
76+
for (let i = 0; i < vestingData.length; i++) {
77+
// Get the corresponding ISO share information for the grant and year
78+
const isoInfo = isoData.find((iso) => iso.Grant === vestingData[i].Grant && iso.Year === vestingData[i].Year);
79+
if (!isoInfo) {
80+
throw new Error(`ISO data not found for Grant ${vestingData[i].Grant} and Year ${vestingData[i].Year}`);
81+
}
82+
const grantYearKey = `${vestingData[i].Grant}-${vestingData[i].Year}`;
83+
// Initialize ISO Remaining at the start of the year
84+
if (!(grantYearKey in isoRemainingByGrantAndYear)) {
85+
isoRemainingByGrantAndYear[grantYearKey] = isoInfo.ISOShares;
86+
}
87+
// Calculate ISO Used as the minimum of Amount Vested and ISO Remaining
88+
const isoUsed = Math.min(vestingData[i]["Event Quantity"], isoRemainingByGrantAndYear[grantYearKey]);
89+
// Calculate NSO if ISO Remaining is zero
90+
const nso = isoRemainingByGrantAndYear[grantYearKey] === 0 ? vestingData[i]["Event Quantity"] : Math.max(0, vestingData[i]["Event Quantity"] - isoUsed);
91+
// Update ISO Remaining
92+
const isoRemaining = isoRemainingByGrantAndYear[grantYearKey] - isoUsed;
93+
// Store the updated ISO Remaining for the next rows in the same year
94+
isoRemainingByGrantAndYear[grantYearKey] = isoRemaining;
95+
// Add the row with the new ISO and NSO columns
96+
result.push(Object.assign(Object.assign({}, vestingData[i]), { ISO: isoUsed, NSO: nso, ISORemaining: isoRemaining }));
97+
}
98+
return result;
99+
}
100+
const isoNsoCalculator = (packagePath, stakeholderId, capacity) => {
101+
const ocfPackage = (0, read_ocf_package_1.readOcfPackage)(packagePath);
102+
const valuations = ocfPackage.valuations;
103+
const transactions = ocfPackage.transactions;
104+
const equityCompensationIssuances = transactions.filter((transaction) => transaction.stakeholder_id === stakeholderId && transaction.object_type === "TX_EQUITY_COMPENSATION_ISSUANCE");
105+
if (equityCompensationIssuances.length === 0) {
106+
throw new Error("No equity compensation issuances found for stakeholder");
107+
}
108+
const sortedIssuances = equityCompensationIssuances.sort((a, b) => a.date.localeCompare(b.date));
109+
const combinedYearTable = [];
110+
const combinedGrants = [];
111+
let vestedByYearTable = [];
112+
sortedIssuances.forEach((issuance) => {
113+
const vestingSchedule = (0, vesting_schedule_generator_1.generateSchedule)(packagePath, issuance.security_id);
114+
const vestingScheduleWithYearAndGrantId = addYearAndGrantId(vestingSchedule, issuance.id);
115+
combinedGrants.push(vestingScheduleWithYearAndGrantId);
116+
vestedByYearTable = sumByYear(vestingScheduleWithYearAndGrantId);
117+
valuations.forEach((valuation) => {
118+
if (valuation.id === issuance.valuation_id) {
119+
for (let i = 0; i < vestedByYearTable.length; i++) {
120+
vestedByYearTable[i]["FMV"] = parseFloat(valuation.price_per_share.amount);
121+
vestedByYearTable[i]["VestedValue"] = vestedByYearTable[i]["FMV"] * vestedByYearTable[i]["TotalAmountVested"];
122+
}
123+
}
124+
});
125+
combinedYearTable.push(...vestedByYearTable);
126+
});
127+
combinedYearTable.sort((a, b) => a.Year - b.Year);
128+
const updatedVestingData = calculateCapacity(combinedYearTable, capacity);
129+
// Add ISO and NSO shares
130+
const updatedVestingDataWithShares = addSharesColumns(updatedVestingData);
131+
const sortedByGrant = updatedVestingDataWithShares.sort((a, b) => a.Grant.localeCompare(b.Grant));
132+
let result = [];
133+
combinedGrants.forEach((grant) => {
134+
const updatedVestingData = addISOColumns(grant, sortedByGrant);
135+
result.push(updatedVestingData);
136+
});
137+
return result;
138+
};
139+
exports.isoNsoCalculator = isoNsoCalculator;

dist/ocf_snapshot/index.js

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"use strict";
2+
Object.defineProperty(exports, "__esModule", { value: true });
3+
exports.ocfSnapshot = void 0;
4+
const read_ocf_package_1 = require("../read_ocf_package");
5+
const ocf_validator_1 = require("../ocf_validator");
6+
const ocfSnapshot = (packagePath, inputDateStr) => {
7+
const ocfPackage = (0, read_ocf_package_1.readOcfPackage)(packagePath);
8+
const snapshots = (0, ocf_validator_1.ocfValidator)(packagePath).snapshots;
9+
const inputDate = new Date(inputDateStr);
10+
const filteredSnapshots = snapshots.filter((snapshot) => new Date(snapshot.date) <= inputDate);
11+
filteredSnapshots.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
12+
const snapshot = filteredSnapshots.length ? filteredSnapshots[0] : null;
13+
return snapshot;
14+
};
15+
exports.ocfSnapshot = ocfSnapshot;
+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"use strict";
2+
Object.defineProperty(exports, "__esModule", { value: true });
3+
const transaction_types = [
4+
'TX_STOCK_ISSUANCE',
5+
'TX_STOCK_RETRACTION',
6+
'TX_STOCK_ACCEPTANCE',
7+
'TX_STOCK_CANCELLATION',
8+
'TX_STOCK_CONVERSION',
9+
'TX_STOCK_REISSUANCE',
10+
'TX_STOCK_REPURCHASE',
11+
'TX_STOCK_TRANSFER',
12+
'TX_CONVERTIBLE_ISSUANCE',
13+
'TX_CONVERTIBLE_RETRACTION',
14+
'TX_CONVERTIBLE_ACCEPTANCE',
15+
'TX_CONVERTIBLE_CANCELLATION',
16+
'TX_CONVERTIBLE_TRANSFER',
17+
'TX_CONVERTIBLE_CONVERSION',
18+
'TX_WARRANT_ISSUANCE',
19+
'TX_WARRANT_RETRACTION',
20+
'TX_WARRANT_ACCEPTANCE',
21+
'TX_WARRANT_CANCELLATION',
22+
'TX_WARRANT_TRANSFER',
23+
'TX_WARRANT_EXERCISE',
24+
'TX_EQUITY_COMPENSATION_ISSUANCE',
25+
'TX_EQUITY_COMPENSATION_RETRACTION',
26+
'TX_EQUITY_COMPENSATION_ACCEPTANCE',
27+
'TX_EQUITY_COMPENSATION_CANCELLATION',
28+
'TX_EQUITY_COMPENSATION_RELEASE',
29+
'TX_EQUITY_COMPENSATION_TRANSFER',
30+
'TX_EQUITY_COMPENSATION_EXERCISE',
31+
'TX_PLAN_SECURITY_ISSUANCE',
32+
'TX_PLAN_SECURITY_RETRACTION',
33+
'TX_PLAN_SECURITY_ACCEPTANCE',
34+
'TX_PLAN_SECURITY_CANCELLATION',
35+
'TX_PLAN_SECURITY_RELEASE',
36+
'TX_PLAN_SECURITY_TRANSFER',
37+
'TX_PLAN_SECURITY_EXERCISE',
38+
'TX_STOCK_CLASS_CONVERSION_RATIO_ADJUSTMENT',
39+
'TX_STOCK_CLASS_AUTHORIZED_SHARES_ADJUSTMENT',
40+
'TX_STOCK_CLASS_SPLIT',
41+
'TX_STOCK_PLAN_POOL_ADJUSTMENT',
42+
'TX_STOCK_PLAN_RETURN_TO_POOL',
43+
'TX_VESTING_ACCELERATION',
44+
'TX_VESTING_START',
45+
'TX_VESTING_EVENT',
46+
];
47+
const constants = { transaction_types };
48+
exports.default = constants;

dist/ocf_validator/eod.js

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
"use strict";
2+
Object.defineProperty(exports, "__esModule", { value: true });
3+
const RUN_EOD = (context, event) => {
4+
let snapshot = { date: event.date, stockIssuances: [], convertibles: [], warrants: [], equityCompensation: [] };
5+
context.stockIssuances.forEach((issuance) => {
6+
snapshot.stockIssuances.push({
7+
date: issuance.date,
8+
custom_id: issuance.custom_id,
9+
stakeholder: issuance.stakeholder_id,
10+
stock_class: issuance.stock_class_id,
11+
quantity: issuance.quantity,
12+
});
13+
});
14+
context.convertibleIssuances.forEach((issuance) => {
15+
snapshot.convertibles.push({
16+
date: issuance.date,
17+
custom_id: issuance.custom_id,
18+
stakeholder: issuance.stakeholder_id,
19+
purchase_price: issuance.purchase_price,
20+
});
21+
});
22+
context.warrantIssuances.forEach((issuance) => {
23+
snapshot.warrants.push({
24+
date: issuance.date,
25+
custom_id: issuance.custom_id,
26+
stakeholder: issuance.stakeholder_id,
27+
purchase_price: issuance.purchase_price,
28+
});
29+
});
30+
context.equityCompensation.forEach((issuance) => {
31+
snapshot.equityCompensation.push({
32+
date: issuance.date,
33+
custom_id: issuance.custom_id,
34+
stakeholder: issuance.stakeholder_id,
35+
quantity: issuance.quantity,
36+
availableToExercise: issuance.availableToExercise,
37+
});
38+
});
39+
return snapshot;
40+
};
41+
exports.default = RUN_EOD;

0 commit comments

Comments
 (0)