"""Research workspace: turn case notes into analytic artifacts (the sandbox). The tiny head reasons; the workspace materializes. Given a case ledger (NOTE lines, search hits, verdicts) it renders, saves, and returns markdown documents: - timeline events with dates, sorted, source-tagged - evidence claim/evidence/value rows (the discrepancy table) - series two value series as an ASCII chart (pattern display) - crossref a theme/symbol mapped to every source that mentions it Deterministic only - no model inference here. Artifacts are saved under data/artifacts/ so a case produces durable documents, not just chat. """ import json import re from pathlib import Path ROOT = Path(__file__).resolve().parents[1] ART_DIR = ROOT / "data" / "artifacts" DATE_RE = re.compile(r"\b((?:19|20)\d{2}(?:-\d{1,2}(?:-\d{1,2})?)?)\b") VALUE_RE = re.compile(r"\b(\d{1,2}:\d{2}|\d+(?:,\d{3})*\.?\d*%?)\b") SOURCE_RE = re.compile(r"\[([a-z0-9_./-]+)\]|(https?://\S+)") ART_KINDS = ("timeline", "evidence", "series", "crossref") def _slug(s): return re.sub(r"[^a-z0-9]+", "_", s.lower()).strip("_") or "artifact" def save_md(title, md): ART_DIR.mkdir(parents=True, exist_ok=True) path = ART_DIR / f"{_slug(title)}.md" path.write_text(md + ("\n" if not md.endswith("\n") else ""), encoding="utf-8") return path def _src(line): m = SOURCE_RE.search(line) return m.group(1) or m.group(2) if m else "-" def split_rows(ledger): """Split a ledger into (date_rows, value_rows, bare_rows).""" dates, values, bare = [], [], [] for ln in ledger: ln = ln.strip() if not ln: continue dm = DATE_RE.search(ln) if dm: dates.append((dm.group(1), ln, _src(ln))) continue vm = VALUE_RE.search(ln) if vm: values.append((ln, _src(ln))) else: bare.append(ln) dates.sort(key=lambda r: r[0]) return dates, values, bare def render_timeline(ledger, title="Timeline"): rows, _, _ = split_rows(ledger) if not rows: return None out = [f"# {title}", "", "| Date | Event | Source |", "|---|---|---|"] for d, ln, s in rows: out.append(f"| {d} | {ln[:140]} | {s} |") out.append("") out.append("_Ordering is verifiable only where the source records the date; " "gaps are as informative as entries._") return "\n".join(out) def render_evidence(ledger, title="Evidence & Discrepancies"): rows, _, _ = split_rows(ledger) if not rows: return None out = [f"# {title}", "", "| Date | Statement | Source |", "|---|---|---|"] for d, ln, s in rows: out.append(f"| {d} | {ln[:140]} | {s} |") return "\n".join(out) def render_series(series, title="Series Comparison"): """series: list of (label, [numbers]). ASCII bars side by side.""" if not series or len(series) < 2: return None labels = [s[0] for s in series] seqs = [list(s[1]) for s in series] n = min(len(x) for x in seqs) if n == 0: return None out = [f"# {title}", "", f"| {' | '.join(labels)} |", f"|{'---|' * len(labels)}"] for i in range(n): vals = [x[i] for x in seqs] out.append("| " + " | ".join(f"{v:.4g}" for v in vals) + " |") out.append("") out.append("Points (index) " + " ".join(f"[{i}]" for i in range(n))) for j, (lab, seq) in enumerate(series): mx = max(seq) or 1 bars = ["#" * max(1, round(v / mx * 20)) for v in seq] out.append(f"{lab}: " + " ".join(bars)) out.append("") out.append("_The chart only compares values; it asserts nothing about cause._") return "\n".join(out) def render_crossref(theme, lines, title="Cross-Reference"): """theme: a term/symbol; lines: source-tagged ledger/notes.""" out = [f"# {title}", "", f"Theme/symbol: **{theme}**", "", "| Source | Context |", "|---|---|"] hit = 0 low = theme.lower() for ln in lines: if low in ln.lower(): out.append(f"| {_src(ln)} | {ln[:150]} |") hit += 1 if not hit: out.append("| - | (no mention in this case's documents) |") out.append("") out.append("_Absence of a mention is a finding, not an error: note it explicitly._") return "\n".join(out) def synthesize(ledger, title, series=None, theme=None, lines=None): """Compose every artifact available for a case into one saved document.""" arts = [] tl = render_timeline(ledger, title=f"{title} - Timeline") if tl: arts.append(tl) ev = render_evidence(ledger, title=f"{title} - Evidence & Discrepancies") if ev: arts.append(ev) ch = render_series(series, title=f"{title} - Series Comparison") if series else None if ch: arts.append(ch) cr = render_crossref(theme, lines or ledger, title=f"{title} - Cross-Reference") if theme else None if cr: arts.append(cr) if not arts: return None doc = "\n\n---\n\n".join(arts) ART_DIR.mkdir(parents=True, exist_ok=True) path = ART_DIR / f"{_slug(title)}.md" path.write_text(doc + "\n", encoding="utf-8") return str(path), doc def parse_series_arg(arg): """'title | label1:1,2,3 | label2:4,5,6' -> (title, [(label, [nums])]).""" parts = [p.strip() for p in arg.split("|")] title = parts[0] or "Series Comparison" series = [] for p in parts[1:]: if ":" not in p: continue lab, vs = p.split(":", 1) nums = [] for v in vs.replace(" ", "").split(","): try: nums.append(float(v)) except ValueError: pass if nums: series.append((lab.strip(), nums)) return title, series