← Files InvestorListsARCHIVED FILE

skills/il-pipeline/scripts/parse_notes.py

3.26 KB · Nov 10, 2026 · 12:09 UTC

↓ Download file

#!/usr/bin/env python3
"""Parse InvestorLists firm notes into a board. Input: notes.json saved from my_notes
(list of {firm_id, firm_name, text, created_at}). Lines in the shape
  [IL] <skill> | <YYYY-MM-DD> | status=<v> | next=<v> | due=<v> | path=<v> | <free>
are read; other lines are ignored. The latest [IL] line per firm wins.
Outputs board.json: rows (firm, batch, status, last_action, last_date, next, due, path),
due_now (due <= today), silent (no action in --silent-days, full mode only), by_status, by_batch.

Usage:
  python parse_notes.py notes.json board.json --today 2026-10-20 [--silent-days 14]
"""
import argparse
import json
import re
from collections import Counter
from datetime import date, timedelta

LINE = re.compile(r"^\[IL\]\s*(?P<skill>[\w-]+)\s*\|\s*(?P<date>\d{4}-\d{2}-\d{2})\s*\|(?P<rest>.*)$")
KEYS = ("status", "next", "due", "path")
CLOSED = ("passed", "meeting-booked", "second-meeting", "term-sheet")


def parse_line(s):
    m = LINE.match(s.strip())
    if not m:
        return None
    d = {"skill": m.group("skill"), "date": m.group("date"), "free": ""}
    for part in m.group("rest").split("|"):
        part = part.strip()
        if "=" in part and part.split("=")[0].strip() in KEYS:
            k, v = part.split("=", 1)
            d[k.strip()] = v.strip()
        elif part:
            d["free"] = (d["free"] + " " + part).strip()
    return d


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("notes")
    ap.add_argument("out")
    ap.add_argument("--today", required=True)
    ap.add_argument("--silent-days", type=int, default=14)
    a = ap.parse_args()
    today = date.fromisoformat(a.today)
    notes = json.load(open(a.notes))
    latest = {}
    batch_of = {}
    for n in notes:
        fid = str(n.get("firm_id"))
        for line in str(n.get("text", "")).splitlines():
            p = parse_line(line)
            if not p:
                continue
            st = p.get("status", "")
            if st.startswith("shortlisted-b"):
                batch_of[fid] = st.split("shortlisted-b", 1)[1]
            if fid not in latest or p["date"] >= latest[fid]["date"]:
                latest[fid] = {**p, "firm": n.get("firm_name", fid), "firm_id": fid}
    rows = []
    for fid, p in latest.items():
        rows.append({
            "firm": p["firm"], "firm_id": fid, "batch": batch_of.get(fid, ""),
            "status": p.get("status", ""), "last_action": p["skill"], "last_date": p["date"],
            "next": p.get("next", ""), "due": p.get("due", ""), "path": p.get("path", ""),
        })
    due_now = [r for r in rows if r["due"] and r["due"] <= a.today and r["status"] not in CLOSED]
    silent = [r for r in rows if date.fromisoformat(r["last_date"]) <= today - timedelta(days=a.silent_days)
              and r["status"].startswith("touch") and not r["due"]]
    out = {
        "rows": sorted(rows, key=lambda r: (r["due"] or "9999", r["last_date"])),
        "due_now": due_now,
        "silent": silent,
        "by_status": dict(Counter(r["status"] for r in rows)),
        "by_batch": dict(Counter(r["batch"] for r in rows if r["batch"])),
        "count": len(rows),
    }
    json.dump(out, open(a.out, "w"), indent=2)
    print(f"{len(rows)} firms, {len(due_now)} due now, {len(silent)} silent")


if __name__ == "__main__":
    main()

SHA-256: 9f1b81933d601455f18f966160ee01273e8739081aca4e052d247d9855eab829