v0.1.9: per-company progress reviews + deck-quality guidance

progress.py reads the whole graded ledger and answers two questions the
per-deck scorecard can't: is the company actually progressing (composite/
BDEF-category/KPI trajectories, recurring vs resolved flags), and is the
material good enough to judge them by — a deterministic gap engine spots
what the decks are NOT showing (no profitability visibility, untargeted
KPIs, no forward guidance, broken forecast chain, silently dropped KPIs,
thin-evidence BDEF categories, no board asks) and renders each gap as a
concrete, paste-ready request for the next deck.

Served live from the ledger (no GPU) at /api/companies/{slug}/progress(.md),
written to /data/ledger/<slug>/PROGRESS.md + /data/reports/latest-progress.md
after each graded deck, and viewable/downloadable from the dashboard company
card ("View progress review"). 18 new tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jonathan Kirkwood
2026-07-31 20:50:41 -05:00
co-authored by Claude Fable 5
parent 506e6c79bd
commit 9b9c7e58c1
8 changed files with 792 additions and 14 deletions
+20
View File
@@ -21,6 +21,7 @@ import decks
import graders as grader_mod
import jobs
import ledger as ledger_mod
import progress as progress_mod
import serving
DATA_DIR = os.environ.get("BM_DATA_DIR", "/data")
@@ -406,6 +407,25 @@ def delete_company(slug: str, restore_decks: bool = False):
return {"ok": True, "removed": removed, "restored_decks": restored}
def _progress_analysis(slug: str) -> dict:
"""Live progress analysis over the ledger (never stale, no file needed)."""
led = ledger_mod.Ledger(LEDGER_DIR)
company = led.get_company(slug)
if company is None:
raise HTTPException(404, "no such company")
return progress_mod.analyze(company, led.deck_records(slug) or [])
@app.get("/api/companies/{slug}/progress")
def company_progress(slug: str):
return _progress_analysis(os.path.basename(slug))
@app.get("/api/companies/{slug}/progress.md", response_class=PlainTextResponse)
def company_progress_md(slug: str):
return progress_mod.render_progress_md(_progress_analysis(os.path.basename(slug)))
@app.get("/api/companies/{slug}/scorecard", response_class=PlainTextResponse)
def company_scorecard(slug: str):
slug = os.path.basename(slug)