Current task: —

← Session 10 — Website planning + local dashboard build

#387 assistant 2026-08-19 17:56:03.401 UTC · 5045→3354 tok open ⤢
thinking (78 chars)
Now the core: research_data.py (read-only parsers), views.py, api.py, urls.py.
Scaffold done. Now the read-only data layer.
tool: TodoWrite ok
input
{
  "todos": [
    {
      "content": "Write RH-WEBSITE.md plan (with DO-NOT-WRITE rule at top)",
      "status": "completed",
      "activeForm": "Writing RH-WEBSITE.md plan"
    },
    {
      "content": "Install web deps (Django, markdown) + vendor Plotly/MathJax JS",
      "status": "completed",
      "activeForm": "Installing web dependencies"
    },
    {
      "content": "Scaffold Django project in web/",
      "status": "completed",
      "activeForm": "Scaffolding Django project"
    },
    {
      "content": "Implement research data parsers (read-only)",
      "status": "in_progress",
      "activeForm": "Implementing data parsers"
    },
    {
      "content": "Implement views + API endpoints + feedback system",
      "status": "pending",
      "activeForm": "Implementing views and API"
    },
    {
      "content": "Build templates, CSS, JS (sidebar layout, charts, live polling)",
      "status": "pending",
      "activeForm": "Building templates and assets"
    },
    {
      "content": "Migrate DB, start server detached, verify pages",
      "status": "pending",
      "activeForm": "Starting and verifying server"
    },
    {
      "content": "Commit website files (web/ + RH-WEBSITE.md only)",
      "status": "pending",
      "activeForm": "Committing website files"
    }
  ]
}
result 2026-08-19 17:56:03Z → 2026-08-23 13:55:56Z
{
  "oldTodos": [],
  "newTodos": [
    "content='Write LiSeries.lean \u00a70\u2013\u00a72: seriesTerm/seriesFun defs, norm comparison, tail bound, seriesSummable' status='in_progress' active_form='Writing LiSeries.lean core (defs + summability)'",
    "content='\u00a73\u2013\u00a74: finite subtype balls, ball-avoidance at non-zero points, seriesFun analytic off the zeros' status='pending' active_form='Proving analyticity off the zeros'",
    "content='\u00a75: isolation at each zero + reduced-series analyticity + polar-part subtraction' status='pending' active_form='Proving isolation + reduced series'",
    "content='Iterate lake build to GREEN; root-import LiSeries' status='pending' active_form='Building to GREEN'",
    "content='Axiom audit session-028 (3 standard axioms only)' status='pending' active_form='Running axiom audit'",
    "content='Session-end protocol: journal-028, STATE.md, NGUYEN.md, db, commit' status='pending' active_form='Closing session 028'"
  ],
  "cleared": false
}
tool: Write ok
input
{
  "file_path": "D:\\Python\\Quantum\\web\\dashboard\\research_data.py",
  "content": "\"\"\"\nRead-only access layer for the NGUYEN-RH-1 research archive.\n\nHARD RULE (RH-WEBSITE.md \u00a70): every handle opened here is READ-ONLY.\nSQLite is opened with mode=ro; files are opened 'r'. Nothing in this\nmodule may write to REPO_ROOT or RESEARCH_DIR.\n\"\"\"\nimport csv\nimport json\nimport re\nimport sqlite3\nimport subprocess\nfrom datetime import datetime, timezone\nfrom pathlib import Path\n\nimport markdown\nfrom django.conf import settings\n\nREPO = Path(settings.REPO_ROOT)\nRESEARCH = Path(settings.RESEARCH_DIR)\nDATA = RESEARCH / \"numerics\" / \"data\"\nDB = RESEARCH / \"research.db\"\n\nMAX_CHART_POINTS = 3000\nPREVIEW_ROWS = 25\n\n\n# ---------------------------------------------------------------- files\n\ndef read_text(path):\n    return Path(path).read_text(encoding=\"utf-8\", errors=\"replace\")\n\n\ndef mtime(path):\n    p = Path(path)\n    return p.stat().st_mtime if p.exists() else None\n\n\ndef _iso(ts):\n    return datetime.fromtimestamp(ts, tz=timezone.utc).isoformat(timespec=\"seconds\")\n\n\n# ---------------------------------------------------------------- sqlite (read-only)\n\ndef _query(sql, params=()):\n    con = sqlite3.connect(f\"file:{DB}?mode=ro\", uri=True)\n    try:\n        con.row_factory = sqlite3.Row\n        return [dict(r) for r in con.execute(sql, params)]\n    finally:\n        con.close()\n\n\ndef get_results():\n    return _query(\"SELECT * FROM results ORDER BY id\")\n\n\ndef get_experiments():\n    return _query(\"SELECT * FROM experiments ORDER BY id\")\n\n\ndef get_derivations():\n    return _query(\"SELECT * FROM derivations ORDER BY id\")\n\n\ndef get_graveyard():\n    return _query(\"SELECT * FROM graveyard ORDER BY id\")\n\n\ndef get_sessions():\n    return _query(\"SELECT * FROM sessions ORDER BY id\")\n\n\ndef get_result(rid):\n    rows = _query(\"SELECT * FROM results WHERE id = ?\", (rid,))\n    return rows[0] if rows else None\n\n\ndef get_experiment(eid):\n    rows = _query(\"SELECT * FROM experiments WHERE id = ?\", (eid,))\n    return rows[0] if rows else None\n\n\n# ---------------------------------------------------------------- STATE.md / NGUYEN.md\n\ndef parse_state():\n    \"\"\"Split STATE.md into its '## SECTION' blocks.\"\"\"\n    path = REPO / \"STATE.md\"\n    if not path.exists():\n        return {}\n    sections, current = {}, \"_header\"\n    for line in read_text(path).splitlines():\n        m = re.match(r\"^##\\s+(.*)\", line)\n        if m:\n            current = m.group(1).strip()\n            sections[current] = []\n        else:\n            sections.setdefault(current, []).append(line)\n    return {k: \"\\n\".join(v).strip() for k, v in sections.items()}\n\n\ndef parse_checklist():\n    \"\"\"Parse NGUYEN.md \u00a75 phases and their checkbox tasks.\"\"\"\n    path = REPO / \"NGUYEN.md\"\n    if not path.exists():\n        return {\"phases\": [], \"done\": 0, \"partial\": 0, \"open\": 0}\n    text = read_text(path)\n    m = re.search(r\"## 5\\..*?(?=\\n## 6\\.)\", text, re.S)\n    section = m.group(0) if m else text\n\n    phases, phase = [], None\n    totals = {\"done\": 0, \"partial\": 0, \"open\": 0}\n    for line in section.splitlines():\n        pm = re.match(r\"###\\s+(\u2705|\u2b1c|\ud83d\udd36)?\\s*(PHASE\\s+\\d+.*)\", line)\n        if pm:\n            phase = {\"name\": pm.group(2).strip(), \"marker\": pm.group(1) or \"\",\n                     \"done\": 0, \"partial\": 0, \"open\": 0}\n            phases.append(phase)\n            continue\n        tm = re.match(r\"- \\[(x|~| )\\]\", line)\n        if tm:\n            key = {\"x\": \"done\", \"~\": \"partial\", \" \": \"open\"}[tm.group(1)]\n            totals[key] += 1\n            if phase is not None:\n                phase[key] += 1\n    return {\"phases\": phases, **totals}\n\n\n# ---------------------------------------------------------------- markdown\n\n_MATH_RE = re.compile(\n    r\"(\\$\\$.*?\\$\\$|\\\\\\[.*?\\\\\\]|\\\\\\(.*?\\\\\\)|\\$[^$\\n]+?\\$)\", re.S)\n\n\ndef render_markdown(text):\n    \"\"\"Render markdown, protecting $...$ math spans for MathJax.\"\"\"\n    store = []\n\n    def stash(m):\n        store.append(m.group(0))\n        return f\"@@MATH{len(store) - 1}@@\"\n\n    protected = _MATH_RE.sub(stash, text)\n    html = markdown.markdown(\n        protected, extensions=[\"tables\", \"fenced_code\", \"sane_lists\"])\n    for i, s in enumerate(store):\n        html = html.replace(f\"@@MATH{i}@@\", s)\n    return html\n\n\n# ---------------------------------------------------------------- markdown collections\n\nMD_COLLECTIONS = {\n    \"results\": RESEARCH / \"results\",\n    \"derivations\": RESEARCH / \"derivations\",\n    \"graveyard\": RESEARCH / \"graveyard\",\n    \"journal\": RESEARCH / \"journal\",\n    \"resources\": RESEARCH / \"resources\",\n}\n\n_SAFE_NAME = re.compile(r\"^[\\w\\-.()\u2013\u2014' \u039e\u03c0\u03b6\u03bb\u03b8\u03b3=,]+$\")\n\n\ndef collection_list(kind):\n    folder = MD_COLLECTIONS[kind]\n    out = []\n    for p in sorted(folder.glob(\"*.md\")):\n        if p.name == \"index.md\":\n            continue\n        out.append({\n            \"name\": p.stem,\n            \"file\": p.name,\n            \"title\": _first_heading(p) or p.stem,\n            \"mtime\": _iso(p.stat().st_mtime),\n        })\n    return out\n\n\ndef _first_heading(path):\n    try:\n        for line in read_text(path).splitlines():\n            if line.startswith(\"# \"):\n                return line[2:].strip()\n            if line.strip():\n                break\n    except OSError:\n        pass\n    return None\n\n\ndef collection_item(kind, name):\n    \"\"\"Render one markdown file from a collection. Path-traversal safe.\"\"\"\n    folder = MD_COLLECTIONS[kind]\n    if not _SAFE_NAME.match(name):\n        return None\n    path = (folder / (name + \".md\")).resolve()\n    if folder.resolve() not in path.parents or not path.exists():\n        return None\n    return {\n        \"name\": name,\n        \"title\": _first_heading(path) or name,\n        \"html\": render_markdown(read_text(path)),\n        \"mtime\": _iso(path.stat().st_mtime),\n    }\n\n\n# ---------------------------------------------------------------- experiments\n\ndef experiment_sidecar(exp_row):\n    \"\"\"Load the JSON metadata sidecar for an experiment row.\"\"\"\n    jf = exp_row.get(\"json_file\")\n    if not jf:\n        return None\n    path = (REPO / jf).resolve()\n    if REPO.resolve() not in path.parents or not path.exists():\n        return None\n    try:\n        return json.loads(read_text(path))\n    except (json.JSONDecodeError, OSError):\n        return None\n\n\ndef _csv_path(exp_row):\n    cf = exp_row.get(\"csv_file\")\n    if not cf:\n        return None\n    path = (REPO / cf).resolve()\n    if REPO.resolve() not in path.parents or not path.exists():\n        return None\n    return path\n\n\ndef csv_preview(exp_row, n=PREVIEW_ROWS):\n    path = _csv_path(exp_row)\n    if not path:\n        return None\n    with open(path, newline=\"\", encoding=\"utf-8\", errors=\"replace\") as fh:\n        rows = list(csv.reader(fh))\n    if not rows:\n        return None\n    return {\"header\": rows[0], \"rows\": rows[1:n + 1],\n            \"total\": max(0, len(rows) - 1)}\n\n\n# Chart configs: which CSV columns to plot per experiment.\n# y may be a list of column names; log_y switches the y axis to log scale.\nCHARTS = {\n    \"E-002\": {\"title\": \"First 10\u2074 zeta zero ordinates \u03b3\u2099\",\n              \"x\": \"n\", \"y\": [\"gamma_n\"]},\n    \"E-008\": {\"title\": \"Li coefficients \u03bb\u2099 (zero side, n \u2264 2000)\",\n              \"x\": \"n\", \"y\": [\"lambda_n\"]},\n    \"E-009b\": {\"title\": \"Galerkin spectrum \u03bb\u2c7c of Suzuki A_a (log scale)\",\n               \"x\": \"a\", \"y\": [\"lambda_j\"], \"log_y\": True},\n    \"E-011\": {\"title\": \"Li coefficients \u03bb\u2099 (arithmetic side, n \u2264 2000)\",\n              \"x\": \"n\", \"y\": [\"lambda_n\"]},\n    \"E-015\": {\"title\": \"Li coefficients \u03bb\u2099 (arithmetic side, n \u2264 3000)\",\n              \"x\": \"n\", \"y\": [\"lambda_n\"]},\n    \"E-018\": {\"title\": \"Descent profile |M| / |R\u2080| (log scale)\",\n              \"x\": \"x\", \"y\": [\"ratio_M_R0\"], \"log_y\": True},\n    \"E-021\": {\"title\": \"S\u221e(n) and S_f(n) curves\",\n              \"x\": \"n\", \"y\": [\"S_inf\", \"S_f\"]},\n    \"E-023\": {\"title\": \"Margin curve: traced vs measured constants\",\n              \"x\": \"n\", \"y\": [\"margin_traced\", \"margin_measured\"]},\n}\n\n\ndef chart_data(exp_row):\n    \"\"\"Return Plotly-ready series for an experiment, or None.\"\"\"\n    cfg = CHARTS.get(exp_row[\"id\"])\n    path = _csv_path(exp_row)\n    if not path:\n        return None\n    with open(path, newline=\"\", encoding=\"utf-8\", errors=\"replace\") as fh:\n        reader = csv.DictReader(fh)\n        fields = reader.fieldnames or []\n        if cfg is None:\n            nums = _numeric_columns(path, fields)\n            if len(nums) < 2:\n                return None\n            cfg = {\"title\": \"Data plot\", \"x\": nums[0], \"y\": nums[1:3]}\n        if cfg[\"x\"] not in fields:\n            return None\n        cols = {c: [] for c in [cfg[\"x\"], *cfg[\"y\"]]}\n        for row in reader:\n            for c in cols:\n                cols[c].append(row.get(c, \"\"))\n\n    n = len(cols[cfg[\"x\"]])\n    step = max(1, n // MAX_CHART_POINTS)\n    traces = []\n    for col in cfg[\"y\"]:\n        traces.append({\n            \"name\": col,\n            \"x\": _to_floats(cols[cfg[\"x\"]][::step]),\n            \"y\": _to_floats(cols[col][::step]),\n        })\n    return {\"title\": cfg.get(\"title\", \"Data plot\"),\n            \"traces\": traces,\n            \"log_y\": bool(cfg.get(\"log_y\")),\n            \"points\": n, \"shown\": len(traces[0][\"x\"]) if traces else 0}\n\n\ndef _numeric_columns(path, fields):\n    with open(path, newline=\"\", encoding=\"utf-8\", errors=\"replace\") as fh:\n        reader = csv.DictReader(fh)\n        sample = [row for _, row in zip(range(50), reader)]\n    nums = []\n    for f in fields:\n        if sample and all(_is_float(r.get(f, \"\")) for r in sample if r.get(f) != \"\"):\n            nums.append(f)\n    return nums\n\n\ndef _is_float(s):\n    try:\n        float(s)\n        return True\n    except (TypeError, ValueError):\n        return False\n\n\ndef _to_floats(values):\n    out = []\n    for v in values:\n        try:\n            out.append(float(v))\n        except (TypeError, ValueError):\n            out.append(None)\n    return out\n\n\n# ---------------------------------------------------------------- live runs / git\n\ndef run_logs():\n    out = []\n    for p in sorted(DATA.glob(\"*-run.log\")):\n        st = p.stat()\n        out.append({\"name\": p.name, \"size\": st.st_size,\n                    \"mtime\": st.st_mtime, \"mtime_iso\": _iso(st.st_mtime)})\n    return out\n\n\n_LOG_NAME = re.compile(r\"^[\\w-]+-run\\.log$\")\n\n\ndef tail_log(name, n=200):\n    \"\"\"Last n lines of a run log. Whitelisted to *-run.log inside DATA.\"\"\"\n    if not _LOG_NAME.match(name):\n        return None\n    path = (DATA / name).resolve()\n    if DATA.resolve() not in path.parents or not path.exists():\n        return None\n    lines = read_text(path).splitlines()\n    return {\"name\": name, \"lines\": lines[-n:], \"total\": len(lines),\n            \"mtime\": _iso(path.stat().st_mtime)}\n\n\ndef git_log(n=12):\n    try:\n        out = subprocess.run(\n            [\"git\", \"-C\", str(REPO), \"log\", \"--oneline\", \"-n\", str(n)],\n            capture_output=True, text=True, timeout=10)\n        return [l for l in out.stdout.splitlines() if l.strip()]\n    except (OSError, subprocess.SubprocessError):\n        return []\n\n\ndef summary_stats():\n    return {\n        \"results\": len(get_results()),\n        \"experiments\": len(get_experiments()),\n        \"derivations\": len(get_derivations()),\n        \"graveyard\": len(get_graveyard()),\n        \"sessions\": len(get_sessions()),\n        \"checklist\": parse_checklist(),\n    }\n"
}
result 2026-08-19 17:56:03Z → 2026-08-27 05:07:19Z
{
  "type": "create",
  "filePath": "D:\\Python\\Quantum\\research\\paper\\build_letter_docx.py",
  "content": "# build_letter_docx.py \u2014 one-off: Letter_To_Professor_Lagarias.txt -> formal letter .docx\n# Reuses the LaTeX->OMML machinery from build_note_docx.py.\nimport re\nimport sys\nfrom pathlib import Path\n\nfrom docx import Document\n\nsys.path.insert(0, str(Path(__file__).parent))\nfrom build_note_docx import add_display, add_rich, flush_para  # noqa: E402\n\nSRC = Path(r\"D:\\Python\\Quantum\\research\\paper\\Letter_To_Professor_Lagarias.txt\")\nOUT = SRC.with_suffix(\".docx\")\n\nLETTER_DATE = \"August 27, 2026\"\nSENDER = [\"Dan T. Nguyen\", \"Independent researcher\"]\nRECIPIENT = [\"Professor Jeffrey C. Lagarias\",\n             \"Department of Mathematics\",\n             \"University of Michigan\",\n             \"Ann Arbor, MI 48109\"]\n\n\ndef block_para(doc, lines, space_before=None):\n    p = doc.add_paragraph()\n    if space_before is not None:\n        p.paragraph_format.space_before = space_before\n    for i, ln in enumerate(lines):\n        if i:\n            p.add_run().add_break()\n        p.add_run(ln)\n    return p\n\n\ndef main():\n    doc = Document()\n    doc.core_properties.title = \"Letter to Professor Lagarias\"\n    doc.core_properties.author = \"Dan T. Nguyen\"\n\n    # --- formal letter head ---\n    block_para(doc, SENDER)\n    block_para(doc, [LETTER_DATE]).paragraph_format.space_before = __import__(\n        \"docx\").shared.Pt(18)\n    block_para(doc, RECIPIENT).paragraph_format.space_before = __import__(\n        \"docx\").shared.Pt(18)\n    doc.add_paragraph()  # blank line before salutation\n\n    lines = SRC.read_text(encoding=\"utf-8\").splitlines()\n\n    # salutation (source line 1)\n    doc.add_paragraph(lines[0].strip())\n\n    # body: after salutation up to \"Sincerely,\"\n    closing_idx = next(i for i, ln in enumerate(lines)\n                       if ln.strip() == \"Sincerely,\")\n    body = lines[2:closing_idx]\n\n    para, i, nmath = [], 0, 0\n    while i < len(body):\n        s = body[i].strip()\n        if s == \"$$\":\n            flush_para(doc, para)\n            para = []\n            blk, i = [], i + 1\n            while i < len(body) and body[i].strip() != \"$$\":\n                blk.append(body[i].strip())\n                i += 1\n            i += 1\n            add_display(doc, \" \".join(blk))\n            nmath += 1\n            continue\n        if re.match(r\"^\\d+\\.\\s\", s):\n            flush_para(doc, para)\n            para = []\n            add_rich(doc.add_paragraph(style=\"List Number\"),\n                     re.sub(r\"^\\d+\\.\\s*\", \"\", s))\n        elif s == \"\":\n            flush_para(doc, para)\n            para = []\n        else:\n            para.append(body[i])\n        i += 1\n    flush_para(doc, para)\n\n    # --- closing ---\n    doc.add_paragraph(\"Sincerely,\")\n    doc.add_paragraph()\n    doc.add_paragraph()\n    sig = doc.add_paragraph()\n    for j, ln in enumerate([ln.strip() for ln in lines[closing_idx + 1:]\n                            if ln.strip()]):\n        if j:\n            sig.add_run().add_break()\n        add_rich(sig, ln)\n\n    doc.save(OUT)\n    print(f\"saved {OUT} ({nmath} display equations)\")\n\n    bad = 0\n    chk = Document(str(OUT))\n    for p in chk.paragraphs:\n        t = \"\".join(r.text for r in p.runs)\n        if \"\\\\(\" in t or \"\\\\frac\" in t or \"\\\\tau\" in t:\n            print(f\"  !! RAW LATEX LEFT: {t[:70]}\")\n            bad += 1\n    print(\"verification: clean\" if bad == 0 else f\"verification: {bad} problems\")\n\n\nif __name__ == \"__main__\":\n    main()\n",
  "structuredPatch": [],
  "originalFile": null
}
raw content_json (database row)
[{"type":"thinking","thinking":"Now the core: research_data.py (read-only parsers), views.py, api.py, urls.py.","signature":""},{"type":"text","text":"Scaffold done. Now the read-only data layer."},{"type":"tool_use","id":"TodoWrite_33","name":"TodoWrite","input":{"todos":[{"content":"Write RH-WEBSITE.md plan (with DO-NOT-WRITE rule at top)","status":"completed","activeForm":"Writing RH-WEBSITE.md plan"},{"content":"Install web deps (Django, markdown) + vendor Plotly/MathJax JS","status":"completed","activeForm":"Installing web dependencies"},{"content":"Scaffold Django project in web/","status":"completed","activeForm":"Scaffolding Django project"},{"content":"Implement research data parsers (read-only)","status":"in_progress","activeForm":"Implementing data parsers"},{"content":"Implement views + API endpoints + feedback system","status":"pending","activeForm":"Implementing views and API"},{"content":"Build templates, CSS, JS (sidebar layout, charts, live polling)","status":"pending","activeForm":"Building templates and assets"},{"content":"Migrate DB, start server detached, verify pages","status":"pending","activeForm":"Starting and verifying server"},{"content":"Commit website files (web/ + RH-WEBSITE.md only)","status":"pending","activeForm":"Committing website files"}]},"extra_content":null},{"type":"tool_use","id":"Write_34","name":"Write","input":{"file_path":"D:\\Python\\Quantum\\web\\dashboard\\research_data.py","content":"\"\"\"\nRead-only access layer for the NGUYEN-RH-1 research archive.\n\nHARD RULE (RH-WEBSITE.md §0): every handle opened here is READ-ONLY.\nSQLite is opened with mode=ro; files are opened 'r'. Nothing in this\nmodule may write to REPO_ROOT or RESEARCH_DIR.\n\"\"\"\nimport csv\nimport json\nimport re\nimport sqlite3\nimport subprocess\nfrom datetime import datetime, timezone\nfrom pathlib import Path\n\nimport markdown\nfrom django.conf import settings\n\nREPO = Path(settings.REPO_ROOT)\nRESEARCH = Path(settings.RESEARCH_DIR)\nDATA = RESEARCH / \"numerics\" / \"data\"\nDB = RESEARCH / \"research.db\"\n\nMAX_CHART_POINTS = 3000\nPREVIEW_ROWS = 25\n\n\n# ---------------------------------------------------------------- files\n\ndef read_text(path):\n    return Path(path).read_text(encoding=\"utf-8\", errors=\"replace\")\n\n\ndef mtime(path):\n    p = Path(path)\n    return p.stat().st_mtime if p.exists() else None\n\n\ndef _iso(ts):\n    return datetime.fromtimestamp(ts, tz=timezone.utc).isoformat(timespec=\"seconds\")\n\n\n# ---------------------------------------------------------------- sqlite (read-only)\n\ndef _query(sql, params=()):\n    con = sqlite3.connect(f\"file:{DB}?mode=ro\", uri=True)\n    try:\n        con.row_factory = sqlite3.Row\n        return [dict(r) for r in con.execute(sql, params)]\n    finally:\n        con.close()\n\n\ndef get_results():\n    return _query(\"SELECT * FROM results ORDER BY id\")\n\n\ndef get_experiments():\n    return _query(\"SELECT * FROM experiments ORDER BY id\")\n\n\ndef get_derivations():\n    return _query(\"SELECT * FROM derivations ORDER BY id\")\n\n\ndef get_graveyard():\n    return _query(\"SELECT * FROM graveyard ORDER BY id\")\n\n\ndef get_sessions():\n    return _query(\"SELECT * FROM sessions ORDER BY id\")\n\n\ndef get_result(rid):\n    rows = _query(\"SELECT * FROM results WHERE id = ?\", (rid,))\n    return rows[0] if rows else None\n\n\ndef get_experiment(eid):\n    rows = _query(\"SELECT * FROM experiments WHERE id = ?\", (eid,))\n    return rows[0] if rows else None\n\n\n# ---------------------------------------------------------------- STATE.md / NGUYEN.md\n\ndef parse_state():\n    \"\"\"Split STATE.md into its '## SECTION' blocks.\"\"\"\n    path = REPO / \"STATE.md\"\n    if not path.exists():\n        return {}\n    sections, current = {}, \"_header\"\n    for line in read_text(path).splitlines():\n        m = re.match(r\"^##\\s+(.*)\", line)\n        if m:\n            current = m.group(1).strip()\n            sections[current] = []\n        else:\n            sections.setdefault(current, []).append(line)\n    return {k: \"\\n\".join(v).strip() for k, v in sections.items()}\n\n\ndef parse_checklist():\n    \"\"\"Parse NGUYEN.md §5 phases and their checkbox tasks.\"\"\"\n    path = REPO / \"NGUYEN.md\"\n    if not path.exists():\n        return {\"phases\": [], \"done\": 0, \"partial\": 0, \"open\": 0}\n    text = read_text(path)\n    m = re.search(r\"## 5\\..*?(?=\\n## 6\\.)\", text, re.S)\n    section = m.group(0) if m else text\n\n    phases, phase = [], None\n    totals = {\"done\": 0, \"partial\": 0, \"open\": 0}\n    for line in section.splitlines():\n        pm = re.match(r\"###\\s+(✅|⬜|🔶)?\\s*(PHASE\\s+\\d+.*)\", line)\n        if pm:\n            phase = {\"name\": pm.group(2).strip(), \"marker\": pm.group(1) or \"\",\n                     \"done\": 0, \"partial\": 0, \"open\": 0}\n            phases.append(phase)\n            continue\n        tm = re.match(r\"- \\[(x|~| )\\]\", line)\n        if tm:\n            key = {\"x\": \"done\", \"~\": \"partial\", \" \": \"open\"}[tm.group(1)]\n            totals[key] += 1\n            if phase is not None:\n                phase[key] += 1\n    return {\"phases\": phases, **totals}\n\n\n# ---------------------------------------------------------------- markdown\n\n_MATH_RE = re.compile(\n    r\"(\\$\\$.*?\\$\\$|\\\\\\[.*?\\\\\\]|\\\\\\(.*?\\\\\\)|\\$[^$\\n]+?\\$)\", re.S)\n\n\ndef render_markdown(text):\n    \"\"\"Render markdown, protecting $...$ math spans for MathJax.\"\"\"\n    store = []\n\n    def stash(m):\n        store.append(m.group(0))\n        return f\"@@MATH{len(store) - 1}@@\"\n\n    protected = _MATH_RE.sub(stash, text)\n    html = markdown.markdown(\n        protected, extensions=[\"tables\", \"fenced_code\", \"sane_lists\"])\n    for i, s in enumerate(store):\n        html = html.replace(f\"@@MATH{i}@@\", s)\n    return html\n\n\n# ---------------------------------------------------------------- markdown collections\n\nMD_COLLECTIONS = {\n    \"results\": RESEARCH / \"results\",\n    \"derivations\": RESEARCH / \"derivations\",\n    \"graveyard\": RESEARCH / \"graveyard\",\n    \"journal\": RESEARCH / \"journal\",\n    \"resources\": RESEARCH / \"resources\",\n}\n\n_SAFE_NAME = re.compile(r\"^[\\w\\-.()–—' Ξπζλθγ=,]+$\")\n\n\ndef collection_list(kind):\n    folder = MD_COLLECTIONS[kind]\n    out = []\n    for p in sorted(folder.glob(\"*.md\")):\n        if p.name == \"index.md\":\n            continue\n        out.append({\n            \"name\": p.stem,\n            \"file\": p.name,\n            \"title\": _first_heading(p) or p.stem,\n            \"mtime\": _iso(p.stat().st_mtime),\n        })\n    return out\n\n\ndef _first_heading(path):\n    try:\n        for line in read_text(path).splitlines():\n            if line.startswith(\"# \"):\n                return line[2:].strip()\n            if line.strip():\n                break\n    except OSError:\n        pass\n    return None\n\n\ndef collection_item(kind, name):\n    \"\"\"Render one markdown file from a collection. Path-traversal safe.\"\"\"\n    folder = MD_COLLECTIONS[kind]\n    if not _SAFE_NAME.match(name):\n        return None\n    path = (folder / (name + \".md\")).resolve()\n    if folder.resolve() not in path.parents or not path.exists():\n        return None\n    return {\n        \"name\": name,\n        \"title\": _first_heading(path) or name,\n        \"html\": render_markdown(read_text(path)),\n        \"mtime\": _iso(path.stat().st_mtime),\n    }\n\n\n# ---------------------------------------------------------------- experiments\n\ndef experiment_sidecar(exp_row):\n    \"\"\"Load the JSON metadata sidecar for an experiment row.\"\"\"\n    jf = exp_row.get(\"json_file\")\n    if not jf:\n        return None\n    path = (REPO / jf).resolve()\n    if REPO.resolve() not in path.parents or not path.exists():\n        return None\n    try:\n        return json.loads(read_text(path))\n    except (json.JSONDecodeError, OSError):\n        return None\n\n\ndef _csv_path(exp_row):\n    cf = exp_row.get(\"csv_file\")\n    if not cf:\n        return None\n    path = (REPO / cf).resolve()\n    if REPO.resolve() not in path.parents or not path.exists():\n        return None\n    return path\n\n\ndef csv_preview(exp_row, n=PREVIEW_ROWS):\n    path = _csv_path(exp_row)\n    if not path:\n        return None\n    with open(path, newline=\"\", encoding=\"utf-8\", errors=\"replace\") as fh:\n        rows = list(csv.reader(fh))\n    if not rows:\n        return None\n    return {\"header\": rows[0], \"rows\": rows[1:n + 1],\n            \"total\": max(0, len(rows) - 1)}\n\n\n# Chart configs: which CSV columns to plot per experiment.\n# y may be a list of column names; log_y switches the y axis to log scale.\nCHARTS = {\n    \"E-002\": {\"title\": \"First 10⁴ zeta zero ordinates γₙ\",\n              \"x\": \"n\", \"y\": [\"gamma_n\"]},\n    \"E-008\": {\"title\": \"Li coefficients λₙ (zero side, n ≤ 2000)\",\n              \"x\": \"n\", \"y\": [\"lambda_n\"]},\n    \"E-009b\": {\"title\": \"Galerkin spectrum λⱼ of Suzuki A_a (log scale)\",\n               \"x\": \"a\", \"y\": [\"lambda_j\"], \"log_y\": True},\n    \"E-011\": {\"title\": \"Li coefficients λₙ (arithmetic side, n ≤ 2000)\",\n              \"x\": \"n\", \"y\": [\"lambda_n\"]},\n    \"E-015\": {\"title\": \"Li coefficients λₙ (arithmetic side, n ≤ 3000)\",\n              \"x\": \"n\", \"y\": [\"lambda_n\"]},\n    \"E-018\": {\"title\": \"Descent profile |M| / |R₀| (log scale)\",\n              \"x\": \"x\", \"y\": [\"ratio_M_R0\"], \"log_y\": True},\n    \"E-021\": {\"title\": \"S∞(n) and S_f(n) curves\",\n              \"x\": \"n\", \"y\": [\"S_inf\", \"S_f\"]},\n    \"E-023\": {\"title\": \"Margin curve: traced vs measured constants\",\n              \"x\": \"n\", \"y\": [\"margin_traced\", \"margin_measured\"]},\n}\n\n\ndef chart_data(exp_row):\n    \"\"\"Return Plotly-ready series for an experiment, or None.\"\"\"\n    cfg = CHARTS.get(exp_row[\"id\"])\n    path = _csv_path(exp_row)\n    if not path:\n        return None\n    with open(path, newline=\"\", encoding=\"utf-8\", errors=\"replace\") as fh:\n        reader = csv.DictReader(fh)\n        fields = reader.fieldnames or []\n        if cfg is None:\n            nums = _numeric_columns(path, fields)\n            if len(nums) < 2:\n                return None\n            cfg = {\"title\": \"Data plot\", \"x\": nums[0], \"y\": nums[1:3]}\n        if cfg[\"x\"] not in fields:\n            return None\n        cols = {c: [] for c in [cfg[\"x\"], *cfg[\"y\"]]}\n        for row in reader:\n            for c in cols:\n                cols[c].append(row.get(c, \"\"))\n\n    n = len(cols[cfg[\"x\"]])\n    step = max(1, n // MAX_CHART_POINTS)\n    traces = []\n    for col in cfg[\"y\"]:\n        traces.append({\n            \"name\": col,\n            \"x\": _to_floats(cols[cfg[\"x\"]][::step]),\n            \"y\": _to_floats(cols[col][::step]),\n        })\n    return {\"title\": cfg.get(\"title\", \"Data plot\"),\n            \"traces\": traces,\n            \"log_y\": bool(cfg.get(\"log_y\")),\n            \"points\": n, \"shown\": len(traces[0][\"x\"]) if traces else 0}\n\n\ndef _numeric_columns(path, fields):\n    with open(path, newline=\"\", encoding=\"utf-8\", errors=\"replace\") as fh:\n        reader = csv.DictReader(fh)\n        sample = [row for _, row in zip(range(50), reader)]\n    nums = []\n    for f in fields:\n        if sample and all(_is_float(r.get(f, \"\")) for r in sample if r.get(f) != \"\"):\n            nums.append(f)\n    return nums\n\n\ndef _is_float(s):\n    try:\n        float(s)\n        return True\n    except (TypeError, ValueError):\n        return False\n\n\ndef _to_floats(values):\n    out = []\n    for v in values:\n        try:\n            out.append(float(v))\n        except (TypeError, ValueError):\n            out.append(None)\n    return out\n\n\n# ---------------------------------------------------------------- live runs / git\n\ndef run_logs():\n    out = []\n    for p in sorted(DATA.glob(\"*-run.log\")):\n        st = p.stat()\n        out.append({\"name\": p.name, \"size\": st.st_size,\n                    \"mtime\": st.st_mtime, \"mtime_iso\": _iso(st.st_mtime)})\n    return out\n\n\n_LOG_NAME = re.compile(r\"^[\\w-]+-run\\.log$\")\n\n\ndef tail_log(name, n=200):\n    \"\"\"Last n lines of a run log. Whitelisted to *-run.log inside DATA.\"\"\"\n    if not _LOG_NAME.match(name):\n        return None\n    path = (DATA / name).resolve()\n    if DATA.resolve() not in path.parents or not path.exists():\n        return None\n    lines = read_text(path).splitlines()\n    return {\"name\": name, \"lines\": lines[-n:], \"total\": len(lines),\n            \"mtime\": _iso(path.stat().st_mtime)}\n\n\ndef git_log(n=12):\n    try:\n        out = subprocess.run(\n            [\"git\", \"-C\", str(REPO), \"log\", \"--oneline\", \"-n\", str(n)],\n            capture_output=True, text=True, timeout=10)\n        return [l for l in out.stdout.splitlines() if l.strip()]\n    except (OSError, subprocess.SubprocessError):\n        return []\n\n\ndef summary_stats():\n    return {\n        \"results\": len(get_results()),\n        \"experiments\": len(get_experiments()),\n        \"derivations\": len(get_derivations()),\n        \"graveyard\": len(get_graveyard()),\n        \"sessions\": len(get_sessions()),\n        \"checklist\": parse_checklist(),\n    }\n"},"extra_content":null}]