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
@@ -0,0 +1,180 @@
|
||||
"""The per-company running score ledger under /data/ledger.
|
||||
|
||||
Layout:
|
||||
/data/ledger/<slug>/company.json identity, aliases, pinned targets,
|
||||
extracted forward targets, history
|
||||
/data/ledger/<slug>/decks/<id>.json full scoring record per graded deck
|
||||
|
||||
Config (bm_config `companies`) is the source of truth for name/aliases/pinned
|
||||
targets; extracted_targets and history are owned by the grading pipeline.
|
||||
Re-graded decks supersede (rename, never delete) the previous record.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import decks as decks_mod
|
||||
|
||||
|
||||
def atomic_write_json(path: str, obj) -> None:
|
||||
"""Write JSON via temp file + os.replace so readers never see a torn file."""
|
||||
tmp = f"{path}.tmp"
|
||||
with open(tmp, "w", encoding="utf-8") as f:
|
||||
json.dump(obj, f, indent=2, sort_keys=False)
|
||||
f.write("\n")
|
||||
os.replace(tmp, path)
|
||||
|
||||
|
||||
def parse_alias_lines(text: str) -> dict:
|
||||
"""Parse newline-separated "canonical=alias1;alias2" lines into a dict."""
|
||||
out: dict[str, list[str]] = {}
|
||||
for line in (text or "").splitlines():
|
||||
line = line.strip()
|
||||
if not line or "=" not in line:
|
||||
continue
|
||||
canonical, _, rest = line.partition("=")
|
||||
canonical = canonical.strip().lower()
|
||||
aliases = [a.strip() for a in rest.split(";") if a.strip()]
|
||||
if canonical and aliases:
|
||||
out[canonical] = aliases
|
||||
return out
|
||||
|
||||
|
||||
class Ledger:
|
||||
def __init__(self, base_dir: str):
|
||||
self.base = base_dir
|
||||
os.makedirs(self.base, exist_ok=True)
|
||||
|
||||
# ------------------------------------------------------------- paths
|
||||
def _company_dir(self, slug: str) -> str:
|
||||
return os.path.join(self.base, slug)
|
||||
|
||||
def _company_path(self, slug: str) -> str:
|
||||
return os.path.join(self._company_dir(slug), "company.json")
|
||||
|
||||
def _decks_dir(self, slug: str) -> str:
|
||||
return os.path.join(self._company_dir(slug), "decks")
|
||||
|
||||
# ------------------------------------------------------------- companies
|
||||
def ensure_company(self, slug: str, name: str | None = None) -> dict:
|
||||
"""Load the company, creating a skeleton entry on first sight."""
|
||||
existing = self.get_company(slug)
|
||||
if existing is not None:
|
||||
return existing
|
||||
company = {
|
||||
"schema_version": 1,
|
||||
"slug": slug,
|
||||
"name": name or slug,
|
||||
"auto_created": name is None,
|
||||
"kpi_aliases": {},
|
||||
"pinned_targets": [],
|
||||
"extracted_targets": {},
|
||||
"history": [],
|
||||
}
|
||||
os.makedirs(self._company_dir(slug), exist_ok=True)
|
||||
atomic_write_json(self._company_path(slug), company)
|
||||
return company
|
||||
|
||||
def get_company(self, slug: str) -> dict | None:
|
||||
try:
|
||||
with open(self._company_path(slug), encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except (FileNotFoundError, json.JSONDecodeError):
|
||||
return None
|
||||
|
||||
def all_slugs(self) -> list[str]:
|
||||
if not os.path.isdir(self.base):
|
||||
return []
|
||||
return sorted(d for d in os.listdir(self.base)
|
||||
if os.path.isfile(self._company_path(d)))
|
||||
|
||||
def all_companies(self) -> list[dict]:
|
||||
return [c for c in (self.get_company(s) for s in self.all_slugs()) if c]
|
||||
|
||||
def merge_config_companies(self, companies_cfg: list) -> None:
|
||||
"""Config wins for name/aliases/pinned targets; ledger keeps the rest."""
|
||||
for cc in companies_cfg or []:
|
||||
name = (cc.get("name") or "").strip()
|
||||
slug = (cc.get("slug") or "").strip() or decks_mod.slugify(name)
|
||||
company = self.ensure_company(slug, name or slug)
|
||||
company["name"] = name or company["name"]
|
||||
company["auto_created"] = False
|
||||
company["kpi_aliases"] = parse_alias_lines(cc.get("kpiAliases") or "")
|
||||
company["pinned_targets"] = list(cc.get("pinnedTargets") or [])
|
||||
atomic_write_json(self._company_path(slug), company)
|
||||
|
||||
# ------------------------------------------------------------- targets
|
||||
def prior_targets(self, slug: str, period: str) -> list[dict]:
|
||||
"""Forward targets an earlier deck set for `period` (this deck's exam)."""
|
||||
company = self.get_company(slug)
|
||||
if not company or not period:
|
||||
return []
|
||||
return (company.get("extracted_targets", {}).get(period) or {}).get("targets", [])
|
||||
|
||||
# ------------------------------------------------------------- decks
|
||||
def record_deck(self, slug: str, record: dict, forward_targets: list[dict]) -> str:
|
||||
"""Persist a scoring record + its forward targets. Returns the deck path.
|
||||
|
||||
A re-graded deck_id supersedes (renames) the old record; the history
|
||||
entry for the same period is replaced; a target period's forward
|
||||
targets are replaced wholesale when set by a newer (or same) deck."""
|
||||
company = self.ensure_company(slug)
|
||||
deck_id = record["deck_id"]
|
||||
ddir = self._decks_dir(slug)
|
||||
os.makedirs(ddir, exist_ok=True)
|
||||
deck_path = os.path.join(ddir, f"{deck_id}.json")
|
||||
if os.path.exists(deck_path):
|
||||
stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
||||
os.replace(deck_path, os.path.join(ddir, f"{deck_id}.superseded-{stamp}.json"))
|
||||
atomic_write_json(deck_path, record)
|
||||
|
||||
period = record.get("period")
|
||||
entry = {"period": period, "composite": record.get("composite"),
|
||||
"graded_at": record.get("graded_at"),
|
||||
"deck_file": f"decks/{deck_id}.json"}
|
||||
history = [h for h in company.get("history", []) if h.get("period") != period]
|
||||
history.append(entry)
|
||||
history.sort(key=lambda h: decks_mod.period_sort_key(h.get("period")))
|
||||
company["history"] = history
|
||||
|
||||
extracted = company.setdefault("extracted_targets", {})
|
||||
from_key = decks_mod.period_sort_key(period)
|
||||
by_period: dict[str, list[dict]] = {}
|
||||
for ft in forward_targets or []:
|
||||
tp = ft.get("target_period")
|
||||
if tp:
|
||||
by_period.setdefault(tp, []).append(ft)
|
||||
for tp, targets in by_period.items():
|
||||
cur = extracted.get(tp)
|
||||
if cur is None or from_key >= decks_mod.period_sort_key(cur.get("from_deck")):
|
||||
extracted[tp] = {"from_deck": period, "targets": targets}
|
||||
|
||||
atomic_write_json(self._company_path(slug), company)
|
||||
return deck_path
|
||||
|
||||
def deck_record(self, slug: str, deck_id: str) -> dict | None:
|
||||
try:
|
||||
with open(os.path.join(self._decks_dir(slug), f"{deck_id}.json"),
|
||||
encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except (FileNotFoundError, json.JSONDecodeError):
|
||||
return None
|
||||
|
||||
def deck_records(self, slug: str) -> list[dict]:
|
||||
"""All live (non-superseded) deck records, oldest period first."""
|
||||
ddir = self._decks_dir(slug)
|
||||
if not os.path.isdir(ddir):
|
||||
return []
|
||||
out: list[dict] = []
|
||||
for fn in sorted(os.listdir(ddir)):
|
||||
if not fn.endswith(".json") or ".superseded-" in fn:
|
||||
continue
|
||||
try:
|
||||
with open(os.path.join(ddir, fn), encoding="utf-8") as f:
|
||||
out.append(json.load(f))
|
||||
except (json.JSONDecodeError, OSError):
|
||||
continue
|
||||
out.sort(key=lambda r: decks_mod.period_sort_key(r.get("period")))
|
||||
return out
|
||||
Reference in New Issue
Block a user