Implement BDEF v1.1 grading: scoring core, per-deck pipeline, ledger, dashboard, StartOS layer
- Deterministic scoring.py (quant 60 / qual 40 / flags -15, profitability heaviest) - Per-company JSON ledger with forecast-target chaining deck N-1 -> N - Single-shot sandbox agent with guided-JSON fallback ladder (no tool loop) - Portfolio dashboard with sparklines, KPI hit rates, BDEF category bars - 48 unit tests green; endpoints smoke-tested; npm check+build green Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
1dde915540
commit
b1d7aed9f4
@@ -5,6 +5,7 @@ to the Sparks, we extract plain text here so that only normalized text (never th
|
||||
original binaries) crosses to the review containers. Supported formats:
|
||||
|
||||
.pdf -> pypdf
|
||||
.pptx -> python-pptx (text frames, tables, chart data, notes)
|
||||
.docx -> python-docx
|
||||
.txt .md .text -> read as UTF-8
|
||||
|
||||
@@ -17,7 +18,7 @@ from __future__ import annotations
|
||||
import os
|
||||
|
||||
TEXT_EXTS = {".txt", ".md", ".text", ".markdown"}
|
||||
SUPPORTED = TEXT_EXTS | {".pdf", ".docx"}
|
||||
SUPPORTED = TEXT_EXTS | {".pdf", ".docx", ".pptx"}
|
||||
|
||||
|
||||
def _extract_pdf(path: str) -> str:
|
||||
@@ -48,6 +49,73 @@ def _extract_docx(path: str) -> str:
|
||||
return "\n".join(lines).strip()
|
||||
|
||||
|
||||
def _pptx_chart_text(shape) -> list[str]:
|
||||
"""Best-effort chart title + series names/values (chart XML varies wildly)."""
|
||||
lines: list[str] = []
|
||||
try:
|
||||
chart = shape.chart
|
||||
try:
|
||||
if chart.has_title and chart.chart_title.has_text_frame:
|
||||
lines.append(f"[chart] {chart.chart_title.text_frame.text}")
|
||||
except Exception:
|
||||
pass
|
||||
for plot in chart.plots:
|
||||
try:
|
||||
cats = [str(c) for c in (plot.categories or [])]
|
||||
except Exception:
|
||||
cats = []
|
||||
for series in plot.series:
|
||||
try:
|
||||
name = str(series.name)
|
||||
except Exception:
|
||||
name = "(series)"
|
||||
try:
|
||||
vals = ["" if v is None else f"{v:g}" for v in series.values]
|
||||
except Exception:
|
||||
vals = []
|
||||
if cats and len(cats) == len(vals):
|
||||
pairs = ", ".join(f"{c}={v}" for c, v in zip(cats, vals))
|
||||
else:
|
||||
pairs = ", ".join(vals)
|
||||
lines.append(f"[chart series] {name}: {pairs}")
|
||||
except Exception:
|
||||
pass
|
||||
return lines
|
||||
|
||||
|
||||
def _extract_pptx(path: str) -> str:
|
||||
from pptx import Presentation
|
||||
|
||||
prs = Presentation(path)
|
||||
parts: list[str] = []
|
||||
for n, slide in enumerate(prs.slides, 1):
|
||||
body: list[str] = []
|
||||
for shape in slide.shapes:
|
||||
if getattr(shape, "has_text_frame", False):
|
||||
txt = shape.text_frame.text.strip()
|
||||
if txt:
|
||||
body.append(txt)
|
||||
if getattr(shape, "has_table", False):
|
||||
for row in shape.table.rows:
|
||||
cells = [c.text.strip() for c in row.cells]
|
||||
if any(cells):
|
||||
body.append(" | ".join(cells))
|
||||
if getattr(shape, "has_chart", False):
|
||||
body.extend(_pptx_chart_text(shape))
|
||||
notes = ""
|
||||
try:
|
||||
if slide.has_notes_slide:
|
||||
notes = (slide.notes_slide.notes_text_frame.text or "").strip()
|
||||
except Exception:
|
||||
pass
|
||||
if notes:
|
||||
body.append(f"--- notes ---\n{notes}")
|
||||
if len("".join(body)) < 20:
|
||||
body.append(f"[low_text_slide: slide {n}]")
|
||||
parts.append(f"\n\n===== slide {n} =====\n" + "\n".join(body))
|
||||
return "".join(parts).strip()
|
||||
|
||||
|
||||
def _extract_text(path: str) -> str:
|
||||
with open(path, errors="replace") as f:
|
||||
return f.read().strip()
|
||||
@@ -57,6 +125,8 @@ def extract_file(path: str) -> str:
|
||||
ext = os.path.splitext(path)[1].lower()
|
||||
if ext == ".pdf":
|
||||
return _extract_pdf(path)
|
||||
if ext == ".pptx":
|
||||
return _extract_pptx(path)
|
||||
if ext == ".docx":
|
||||
return _extract_docx(path)
|
||||
if ext in TEXT_EXTS:
|
||||
@@ -70,20 +140,16 @@ def _safe_name(name: str) -> str:
|
||||
return (keep or "document").replace(" ", "_")
|
||||
|
||||
|
||||
def extract_inbox(inbox: str, out_dir: str, log=print) -> list[dict]:
|
||||
"""Extract every supported file in `inbox` to a .txt in `out_dir`.
|
||||
def extract_files(files: list[str], out_dir: str, log=print) -> list[dict]:
|
||||
"""Extract an explicit list of files (a deck unit) to .txt files in `out_dir`.
|
||||
|
||||
Returns a manifest: [{source, out, chars, ok, error}]. Skips unsupported
|
||||
files (recorded with ok=False) rather than failing the whole job."""
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
manifest: list[dict] = []
|
||||
if not os.path.isdir(inbox):
|
||||
return manifest
|
||||
seen: dict[str, int] = {}
|
||||
for fn in sorted(os.listdir(inbox)):
|
||||
src = os.path.join(inbox, fn)
|
||||
if not os.path.isfile(src):
|
||||
continue
|
||||
for src in files:
|
||||
fn = os.path.basename(src)
|
||||
ext = os.path.splitext(fn)[1].lower()
|
||||
rec = {"source": fn, "out": None, "chars": 0, "ok": False, "error": ""}
|
||||
if ext not in SUPPORTED:
|
||||
@@ -113,3 +179,16 @@ def extract_inbox(inbox: str, out_dir: str, log=print) -> list[dict]:
|
||||
log(f"[extract] {fn} -> {out_name} ({len(text)} chars)")
|
||||
manifest.append(rec)
|
||||
return manifest
|
||||
|
||||
|
||||
def extract_inbox(inbox: str, out_dir: str, log=print) -> list[dict]:
|
||||
"""Extract every supported file in `inbox` to a .txt in `out_dir`.
|
||||
|
||||
Returns a manifest: [{source, out, chars, ok, error}]. Skips unsupported
|
||||
files (recorded with ok=False) rather than failing the whole job."""
|
||||
if not os.path.isdir(inbox):
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
return []
|
||||
files = [os.path.join(inbox, fn) for fn in sorted(os.listdir(inbox))
|
||||
if os.path.isfile(os.path.join(inbox, fn))]
|
||||
return extract_files(files, out_dir, log)
|
||||
|
||||
Reference in New Issue
Block a user