issuance-import
Reads a file the admin already has — typically the Carta importer template they
downloaded from the app — and hands carta-issuance Phase 0.5 a prefilled
knowns.rows. The file feeds the front of the existing pipeline; it does not
add a path around any gate. Phase 1 still resolves stakeholders, Phase 1.5 still
saves and validates, Phase 2 still reviews, Phase 3 is still the only mutate.
Do not invoke this skill directly. Dispatched by carta-issuance
Phase 0.25.
References
| File |
Purpose |
scripts/parse_upload.py |
Does the parsing. Header detection, column mapping, value coercion, security_type detection, and local name→id resolution. The model never hand-parses a workbook — a hand-read column is exactly how a quantity lands in an exercise-price field. |
references/column-map.md |
The header synonyms and value picklists as documentation, for humans and for carta-modify-issuables to reuse rather than fork. |
Two rules the script exists to enforce
- Unresolved is blank, never guessed. A
Vesting Schedule or Share Class
cell that doesn't match a real record exactly (case- and
punctuation-insensitively) leaves the field unset and records an
import_notes entry. There is deliberately no fuzzy matching: an
almost-match issues genuinely wrong terms, and unlike a bad quantity the
server cannot catch it. Do not add fuzzy fallback later without a
product decision.
- Nothing is dropped silently. Unmapped columns, skipped rows, and
uncoercible cells all land in
_import_report.json, and per-field in each
row's import_notes. A dropped Exercise Price column is a wrong-priced
grant the user has no way to notice.
Usage
uv run "${CLAUDE_PLUGIN_ROOT}/skills/carta-issuance/issuance-import/scripts/parse_upload.py" \
--file "<path>" \
[--sheet "<sheet name>"] \
[--reference "$OUT_DIR/_data.json"] \
--out-dir "$OUT_DIR"
--sheet — only needed when the workbook has more than one importable sheet
(the script exits 2 with CANDIDATES=[…]; ask which, then re-run).
--reference — the same JSON build_config.py takes as --data: raw MCP
section envelopes plus stakeholders. Pass it to get ids resolved in the same
run. Omit it and every name comes back as an import_notes entry with the
field left blank, which is correct but makes the admin re-pick by hand — so
pass it whenever Phase 0.5's fetches have landed.
Output
| File |
Contents |
_import_knowns.json |
{security_type, rows, equity_plan_id?, batch_errors?} — merge rows into your _knowns.json |
_import_report.json |
{mode, source_file, sheet, row_count, unmapped_columns, skipped_rows, plan_name, batch_errors, notes_by_row} |
_import_text.txt |
Document mode only — extracted text |
stdout is KEY=value lines (IMPORT_KNOWNS, IMPORT_REPORT, ROW_COUNT,
SECURITY_TYPE). Exit 0 parsed, exit 2 nothing usable — the stderr line says
which (ERROR: or AMBIGUOUS: plus CANDIDATES=).
Spreadsheet mode
Deterministic end to end.
- Header row — scanned across the first 6 rows, taking whichever maps the
most known columns. Carta's importer template puts a paragraph of instructions
in row 1 and the real headers in row 2, so assuming row 1 reads prose as
column names. Needs ≥3 recognizable headers to count as a header row at all.
security_type — decided by header signature first (Exercise Price /
Equity Plan Name / Document Set → option_grant; Share Class /
Legend / Rule 144 Date → certificate), sheet name only as a tiebreak.
A threshold column decides piu outright, before that comparison: the PIU
template also carries an equity plan and a document set, which would otherwise
tie it with the grant signals.
Generic headers (Quantity, Email) are deliberately not signals.
- Multiple importable sheets →
AmbiguousInput, never a guess and never a
merge. A batch is one security type (carta-issuance Hard rule 2).
- Values — dates to
YYYY-MM-DD; numbers stripped of thousands separators,
currency symbols and parenthesised negatives; Individual / Non Individual
to INDIVIDUAL / NON-INDIVIDUAL (hyphen — matches
build_config.py's STAKEHOLDER_KIND_CHOICES, not the Django enum);
relationship, option type and grant reason matched exactly against the
panel's own picklists.
- Out-of-scope rows skipped, not coerced. An importer sheet can carry RSUs,
SARs, CBUs, warrants, RSAs, convertibles. Those rows are skipped with a reason
naming the Drafts UI — never reshaped into a grant of a different type.
- Multiple equity plans in one sheet → a
batch_errors entry. A draft set
is locked to one plan server-side, so this has to surface before the panel
rather than failing at Phase 1.5.
Document mode (.pdf / .docx)
The script extracts text and stops. It does not build rows from prose.
A signed grant doc or board consent has no fixed layout, so turning it into rows
is a judgement call — doing it in the script would mean guessing silently, which
is rule 2 inverted. So: read _import_text.txt, write the rows yourself in
this script's own row schema, and mark every field you filled this way with an
import_notes entry carrying "confidence": "low". The panel renders those as
needs-confirmation, so a misread date is something the admin sees rather than
something that issues.
If the text comes back empty the file is a scan — the script exits 2 saying so.
Route the admin to OCR it or type the values into the panel; never infer values
from a filename.
Row schema
The keys are exactly the ones build_config.py reads off a knowns.rows entry —
ROW_KEYS in parse_upload.py is the authoritative list, and
test_rows_only_carry_keys_build_config_reads fails if a stray key creeps in
(it would survive into the save_drafts payload and be rejected server-side).
Two additions beyond that list:
| Key |
Meaning |
row_key |
Positional r0, r1, … — same contract build_config.py stamps, so Phase 1.5 can re-match a row to its draft_pk |
import_notes |
[{field, raw_value, reason}] — display-only. Never send to any mutate; scripts/serialize_drafts.py strips it at the payload boundary |
Dates in a row are always ISO (YYYY-MM-DD) — that is what <input type="date"> accepts
and reads back. Three of them (grant_expiration_date, vesting_start_date, rule_144_date)
are CharFields the API only takes as MM/DD/YYYY; serialize_drafts.py converts them on the
way out. Do not emit MM/DD/YYYY here — it would break the panel.
1---2name: carta-cap-table-issuance-import3description: Internal file-ingest sub-skill for carta-issuance. Turns an uploaded spreadsheet (.xlsx/.xlsm/.csv/.tsv) or document (.pdf/.docx) into the `knowns.rows` the config panel already consumes, for certificates, option grants and profits interest units. Not invocable directly — dispatched by carta-issuance Phase 0.25.4---56<!-- carta:instrumentation-fallback -->7<IMPORTANT>8`_instrumentation_v2` is anonymous Carta skill-usage telemetry. ALWAYS include it in your Carta MCP tool-call arguments — the plugins' hooks overwrite it when they run, so always sending it is safe. Shape:9`_instrumentation_v2: {"skills": ["<plugin>:<skill>", ...], "model": "<model-id>", "surface": "<value>"}`10`skills` is cumulative for the session: list every Carta skill loaded so far, not only the one driving this call. Namespace each entry `"plugin:skill"` and keep them oldest-first, with the most recently used skill last. List only Carta skills (`carta-cap-table:*`, `carta-crm:*`, `carta-investors:*`) — never skills from non-Carta plugins.11`surface` is the Claude surface you are running in: `"chat"` (claude.ai or the Claude app, i.e. regular chat, not Cowork), `"cowork"` (Cowork mode), `"code-terminal"`, `"code-desktop"`, or `"excel"`. Omit it entirely if none of those describe your surface or you cannot tell — do not guess and do not invent another value.12</IMPORTANT>1314# issuance-import1516Reads a file the admin already has — typically the Carta importer template they17downloaded from the app — and hands `carta-issuance` Phase 0.5 a prefilled18`knowns.rows`. The file feeds the **front** of the existing pipeline; it does not19add a path around any gate. Phase 1 still resolves stakeholders, Phase 1.5 still20saves and validates, Phase 2 still reviews, Phase 3 is still the only mutate.2122**Do not invoke this skill directly.** Dispatched by `carta-issuance`23[Phase 0.25](../SKILL.md#phase-025--ingest-an-uploaded-file).2425## References2627| File | Purpose |28|---|---|29| `scripts/parse_upload.py` | **Does the parsing.** Header detection, column mapping, value coercion, `security_type` detection, and local name→id resolution. The model never hand-parses a workbook — a hand-read column is exactly how a quantity lands in an exercise-price field. |30| `references/column-map.md` | The header synonyms and value picklists as documentation, for humans and for `carta-modify-issuables` to reuse rather than fork. |3132## Two rules the script exists to enforce33341. **Unresolved is blank, never guessed.** A `Vesting Schedule` or `Share Class`35 cell that doesn't match a real record exactly (case- and36 punctuation-insensitively) leaves the field **unset** and records an37 `import_notes` entry. There is deliberately no fuzzy matching: an38 almost-match issues genuinely wrong terms, and unlike a bad quantity the39 server cannot catch it. Do not add fuzzy fallback later without a40 product decision.412. **Nothing is dropped silently.** Unmapped columns, skipped rows, and42 uncoercible cells all land in `_import_report.json`, and per-field in each43 row's `import_notes`. A dropped `Exercise Price` column is a wrong-priced44 grant the user has no way to notice.4546## Usage4748```bash49uv run "${CLAUDE_PLUGIN_ROOT}/skills/carta-issuance/issuance-import/scripts/parse_upload.py" \50 --file "<path>" \51 [--sheet "<sheet name>"] \52 [--reference "$OUT_DIR/_data.json"] \53 --out-dir "$OUT_DIR"54```5556- `--sheet` — only needed when the workbook has more than one importable sheet57 (the script exits 2 with `CANDIDATES=[…]`; ask which, then re-run).58- `--reference` — the **same JSON `build_config.py` takes as `--data`**: raw MCP59 section envelopes plus `stakeholders`. Pass it to get ids resolved in the same60 run. Omit it and every name comes back as an `import_notes` entry with the61 field left blank, which is correct but makes the admin re-pick by hand — so62 pass it whenever Phase 0.5's fetches have landed.6364### Output6566| File | Contents |67|---|---|68| `_import_knowns.json` | `{security_type, rows, equity_plan_id?, batch_errors?}` — merge `rows` into your `_knowns.json` |69| `_import_report.json` | `{mode, source_file, sheet, row_count, unmapped_columns, skipped_rows, plan_name, batch_errors, notes_by_row}` |70| `_import_text.txt` | Document mode only — extracted text |7172stdout is `KEY=value` lines (`IMPORT_KNOWNS`, `IMPORT_REPORT`, `ROW_COUNT`,73`SECURITY_TYPE`). Exit 0 parsed, exit 2 nothing usable — the stderr line says74which (`ERROR:` or `AMBIGUOUS:` plus `CANDIDATES=`).7576## Spreadsheet mode7778Deterministic end to end.7980- **Header row** — scanned across the first 6 rows, taking whichever maps the81 most known columns. Carta's importer template puts a paragraph of instructions82 in row 1 and the real headers in row 2, so assuming row 1 reads prose as83 column names. Needs ≥3 recognizable headers to count as a header row at all.84- **`security_type`** — decided by header signature first (`Exercise Price` /85 `Equity Plan Name` / `Document Set` → `option_grant`; `Share Class` /86 `Legend` / `Rule 144 Date` → `certificate`), sheet name only as a tiebreak.87 A **threshold** column decides `piu` outright, before that comparison: the PIU88 template also carries an equity plan and a document set, which would otherwise89 tie it with the grant signals.90 Generic headers (`Quantity`, `Email`) are deliberately not signals.91- **Multiple importable sheets** → `AmbiguousInput`, never a guess and never a92 merge. A batch is one security type (carta-issuance Hard rule 2).93- **Values** — dates to `YYYY-MM-DD`; numbers stripped of thousands separators,94 currency symbols and parenthesised negatives; `Individual` / `Non Individual`95 to `INDIVIDUAL` / `NON-INDIVIDUAL` (**hyphen** — matches96 `build_config.py`'s `STAKEHOLDER_KIND_CHOICES`, not the Django enum);97 relationship, option type and grant reason matched exactly against the98 panel's own picklists.99- **Out-of-scope rows skipped, not coerced.** An importer sheet can carry RSUs,100 SARs, CBUs, warrants, RSAs, convertibles. Those rows are skipped with a reason101 naming the Drafts UI — never reshaped into a grant of a different type.102- **Multiple equity plans in one sheet** → a `batch_errors` entry. A draft set103 is locked to one plan server-side, so this has to surface before the panel104 rather than failing at Phase 1.5.105106## Document mode (`.pdf` / `.docx`)107108The script extracts text and **stops**. It does not build rows from prose.109110A signed grant doc or board consent has no fixed layout, so turning it into rows111is a judgement call — doing it in the script would mean guessing silently, which112is rule 2 inverted. So: read `_import_text.txt`, write the rows yourself **in113this script's own row schema**, and mark every field you filled this way with an114`import_notes` entry carrying `"confidence": "low"`. The panel renders those as115needs-confirmation, so a misread date is something the admin sees rather than116something that issues.117118If the text comes back empty the file is a scan — the script exits 2 saying so.119Route the admin to OCR it or type the values into the panel; never infer values120from a filename.121122## Row schema123124The keys are exactly the ones `build_config.py` reads off a `knowns.rows` entry —125`ROW_KEYS` in `parse_upload.py` is the authoritative list, and126`test_rows_only_carry_keys_build_config_reads` fails if a stray key creeps in127(it would survive into the `save_drafts` payload and be rejected server-side).128129Two additions beyond that list:130131| Key | Meaning |132|---|---|133| `row_key` | Positional `r0`, `r1`, … — same contract `build_config.py` stamps, so Phase 1.5 can re-match a row to its `draft_pk` |134| `import_notes` | `[{field, raw_value, reason}]` — display-only. **Never** send to any mutate; `scripts/serialize_drafts.py` strips it at the payload boundary |135136Dates in a row are **always ISO** (`YYYY-MM-DD`) — that is what `<input type="date">` accepts137and reads back. Three of them (`grant_expiration_date`, `vesting_start_date`, `rule_144_date`)138are `CharField`s the API only takes as `MM/DD/YYYY`; `serialize_drafts.py` converts them on the139way out. Do not emit `MM/DD/YYYY` here — it would break the panel.