// ============================================================================ // Project : modelmap // File : site/server.js // Purpose : modelmap.io platform — routes and pages (English, light theme) // Author : Simon-Pierre Boucher // Contact : contact@spboucher.ai // Website : https://modelmap.io // Created : 2026-08-12 // Modified : 2026-08-12 // Platform : macOS / Apple Silicon (arm64) — Node.js (deployed on MacLustr) // License : All rights reserved (research code) // ============================================================================ "use strict"; const path = require("path"); const express = require("express"); const C = require("./lib/content"); const R = require("./lib/render"); const Comments = require("./lib/comments"); const Charts = require("./lib/charts"); // ------------------------------------------------------------ chart builders /** Layer-profile charts for a probes atlas entry (real A/B + twin null). */ function probeMapCharts(onlyProp, mapRel) { const m = C.readJson(mapRel || "atlas/qwen3-0.6b-4bit/probes/v1/map.json"); if (!m || !m.properties) return ""; const props = onlyProp ? [onlyProp] : Object.keys(m.properties); return props.map((prop) => { const p = m.properties[prop]; if (!p) return ""; const sel = (rows) => rows.map((r) => r.selectivity_mean); const caption = p.verdict ? `Verdict: ${R.esc(p.verdict)}. Differential (real−twin) claims only; Level 1, ` + "5 seeds, shuffled-label controls inside every probe, token-balanced classes." : "The twin (dashed) matches the trained model — the registered validity gate " + "failed and this map is published as a NEGATIVE result: on these promptsets, probes " + "read the tokenizer + architecture prior, not learned computation. Level 1, 5 seeds, " + "shuffled-label controls inside every probe."; return Charts.layerLineSvg({ title: `${prop} — probe selectivity by layer (trained vs random-init twin)`, yLabel: "selectivity", yMin: 0, yMax: 1, series: [ { label: "trained · set A", color: Charts.S1, values: sel(p.per_layer.A) }, { label: "trained · set B", color: Charts.S2, values: sel(p.per_layer.B) }, { label: "random-init twin", color: Charts.REF, dash: true, ref: true, values: p.twin_null_per_layer_A.map((r) => r.selectivity_mean) }, ], caption, }); }).join(""); } const MAP_V2 = "atlas/qwen3-0.6b-4bit/probes/v2/map.json"; const MAP_INT = "atlas/qwen3-0.6b-4bit/interventions/v1/map.json"; /** Causal direction-erasure BAND map chart (interventions map, run #4 format). */ function interventionsMapCharts(mapRel) { const m = C.readJson(mapRel || MAP_INT); if (!m || !m.per_layer || !m.per_layer.mean) return ""; const all = [...m.per_layer.mean, ...m.per_layer.min, ...m.per_layer.random_direction_damage]; const yMin = Math.floor(Math.min(...all, 0) * 2) / 2 - 0.5; const yMax = Math.ceil(Math.max(...all) * 2) / 2 + 0.5; const band = m.band || {}; return Charts.layerLineSvg({ title: "agreement — causal direction-erasure BAND map (specific damage by layer)", yLabel: "specific margin damage", yMin, yMax, series: [ { label: "mean of 6 fresh sources", color: Charts.S1, values: m.per_layer.mean }, { label: "worst source (min)", color: Charts.S2, values: m.per_layer.min }, { label: "random-direction damage", color: Charts.REF, dash: true, ref: true, values: m.per_layer.random_direction_damage }, ], caption: `The published claim is the BAND (layers ${band.early_layers ? band.early_layers.join("–") : "2–15"}): ` + "erasing the diff-of-means agreement direction at any single early-band layer destroys most of " + "the grammatical margin (baseline " + (m.baseline_margin ? m.baseline_margin.toFixed(2) : "—") + "), replicated across six fresh direction estimates on a fresh behavioral bank (Level 2). " + "The late band is displayed but carries no claim — run #3's per-layer version was refused by " + "the publication gate as estimator-unstable. Anti-correlates with the probes/v2 decodability " + "ranking (survival ledger 0/2): decodability peaks ≠ causal joints.", }); } /** expH charts: warm/cold storage panels + capture-overhead grouped bars. */ function storageChart() { const d = C.expHData(); let html = ""; if (d.warm && d.cold) { const rnd = (rows) => rows.filter((r) => r.op === "random-batch") .map((r) => ({ label: r.format.replace("zarr-", "zarr‑"), value: r.gb_per_s_mean })); html += Charts.barPanelsSvg({ title: "Activation-store random-batch read throughput — the ordering inverts cold", yLabel: "GB/s", unit: "GB/s", panels: [ { title: "warm cache · M5 Max (run #1)", bars: rnd(d.warm) }, { title: "cold cache · M3 Ultra (run #2)", bars: rnd(d.cold) }, ], caption: "Run #2 falsified the registered hypothesis: raw mmap wins warm (memory speed) " + "but collapses cold to page-fault IO (~8–16 KiB, QD 1) while chunked zarr reads 32 MiB " + "blocks. The rule is IO granularity, not the container.", }); } return html; } function captureChart() { const d = C.expHData(); let html = ""; if (d.compute.length) { const key = (c) => `${c.backend}·${c.source}`; const groupsMap = new Map(); for (const c of d.compute) { const ms = 1000 * c.s_per_forward.reduce((a, b) => a + b, 0) / c.s_per_forward.length; if (!groupsMap.has(key(c))) groupsMap.set(key(c), { backend: c.backend, source: c.source, values: {} }); groupsMap.get(key(c)).values[c.mode] = ms; } const label = (g) => g.backend === "torch-mps" ? "torch-MPS (synthetic)" : g.source === "synthetic" ? "MLX (synthetic)" : "MLX (Qwen3-0.6B-4bit)"; html += Charts.groupedBarSvg({ title: "Capture overhead per forward pass — retention is nearly free under MLX", yLabel: "ms / forward", unit: "ms", groups: [...groupsMap.values()].map((g) => ({ label: label(g), values: g.values })), modes: [ { key: "plain", label: "plain", color: Charts.S1 }, { key: "retain", label: "retain", color: Charts.S2 }, { key: "retain+copy+write", label: "retain+copy+write", color: Charts.S3 }, ], caption: "Runs #1 and #3: per-layer retention costs 1.02× (MLX synthetic) and 1.004× " + "(real 4-bit checkpoint) vs 1.22× on torch-MPS; copy+write stays ≤1.5× everywhere. " + "3 repeats per cell; manifests embedded in the raw JSON.", }); } return html; } function expHCharts() { return storageChart() + captureChart(); } const app = express(); const PORT = process.env.PORT || 8140; app.set("trust proxy", true); // behind ngrok — X-Forwarded-For carries the real IP app.use("/static", express.static(path.join(__dirname, "public"), { maxAge: "1h" })); app.use(express.urlencoded({ extended: false, limit: "16kb" })); function page(res, opts) { res.send(R.layout({ ...opts, buildInfo: C.buildInfo() })); } function metaChips(data) { if (!data || !Object.keys(data).length) return ""; const fields = ["document", "author", "created", "modified", "status"]; const chips = fields .filter((f) => data[f]) .map((f) => `${f}${R.esc(data[f])}`) .join(""); return chips ? `
${chips}
` : ""; } // ---------------------------------------------------------------- home app.get("/", (req, res) => { const s = C.stats(); const log = C.parseLogEntries().slice(-3).reverse(); const phases = [ ["Phase 1 — Ultra-deep literature research (§4.1–4.10)", s.notes > 0 ? "in progress" : "pending", s.notes > 0 ? `${s.notes} theme notes · ${s.sources} sources` : "ten mandatory areas, primary sources first"], ["Phase 2 — State-of-the-art map", C.exists("research/state_of_the_art.md") ? "done" : "pending", "technique taxonomy with epistemic status"], ["Phase 3 — Research gaps", s.gaps ? "done" : "pending", s.gaps ? `${s.gaps} falsifiable candidate directions` : "≥20 substantially different candidate directions"], ["Phase 4 — Candidate ranking", C.exists("research/candidate_ranking.md") ? "done" : "pending", "10-axis scoring, 3–5 prototype candidates"], ["Phases 5–6 — Framework & micro-experiments A–H", s.experimentsDone ? "in progress" : "pending", `${s.experimentsDone}/${s.experiments} experiments completed`], ["Phases 7–9 — Prototypes → methodology", C.exists("research/methodology.md") ? "done" : "pending", "evidence-driven; the methodology is a primary deliverable"], ["Phase 10 — Atlas pipeline & this platform", "in progress", `platform live · ${s.atlasEntries} map${s.atlasEntries === 1 ? "" : "s"} published`], ["Phase 11 — Novelty verification", C.exists("research/novelty_check.md") ? "done" : "pending", "assume not novel until evidence suggests otherwise"], ]; const phaseHtml = phases .map( ([name, st, detail]) => `
  • ${name}${detail}
    ${st}
  • ` ) .join(""); const logHtml = log .map( (e) => `

    ${R.esc(e.title)}

    ${R.markdownToHtml(e.body.split("\n").slice(0, 8).join("\n"), "research/LOG.md")}
    Full research log →
    ` ) .join(""); const body = `

    Independent interpretability research · Apple Silicon

    An internal cartography of
    local large language models

    Given an already-trained open-weight model, what can actually be known, measured, localized, and mapped about its internal organization — where knowledge lives, how computation is distributed, which structures are stable across inputs, layers, scales, and model families — using only a consumer-grade Mac? Every map published here is versioned, provenanced, confidence-labeled, and regenerable.

    what a probe reports ≠ what the model computes ≠ what is stable across methods ≠ what survives intervention

    ${s.sources}sources reviewed
    ${s.gaps}research gaps identified
    ${s.experimentsDone}/${s.experiments}experiments completed
    ${s.atlasEntries}atlas maps published

    The evidence standard

    A claim enters the atlas only at the confidence level its evidence supports. Correlational maps and causally-verified maps are never conflated — visually or textually. The two metrics tracked for every map are its replication rate (does it reproduce under resampling of seeds and data?) and its causal confirmation rate (what fraction of localized claims survive intervention?).

    ${R.confidenceLadder()}

    Negative results — techniques whose maps do not replicate — are first-class, publishable findings here.

    Research phases

    ${(() => { const causal = interventionsMapCharts(); const probe2 = probeMapCharts("agreement", MAP_V2); const probe1 = probeMapCharts("lang_id"); const exph = expHCharts(); return causal || probe1 || probe2 || exph ? `

    First measured maps

    Every figure below is regenerated from committed code + versioned results — raw JSON with hardware manifests under Results. Full per-property maps live in the Atlas.

    ${causal}${probe2}${probe1}${exph}
    ` : ""; })()}

    Latest from the research log

    ${logHtml || "

    No log entries yet.

    "}
    `; page(res, { title: "Home", active: "Home", body }); }); // ---------------------------------------------------------------- research index app.get("/research", (req, res) => { const docs = [ ["research/LOG.md", "Research log", "Append-only, auditable record of every question, experiment, result, and decision."], ["research/state_of_the_art.md", "State of the art", "Phase 2 — technique taxonomy with epistemic status (established / contested / debunked-in-part)."], ["research/research_gaps.md", "Research gaps", "Phase 3 — ≥20 falsifiable candidate directions, each with its smallest Mac-runnable falsifying experiment."], ["research/candidate_ranking.md", "Candidate ranking", "Phase 4 — 10-axis scoring and 3–5 selected prototype candidates."], ["research/methodology.md", "Methodology", "Phase 9 — the formal pipeline turning (checkpoint + corpora + budget) into confidence-labeled atlas entries."], ["research/novelty_check.md", "Novelty check", "Phase 11 — final novelty verification (assume not novel until shown otherwise)."], ["research/bibliography.md", "Bibliography", "Every consulted source with URL and access date."], ]; const cards = docs .map(([rel, title, desc]) => { const ok = C.exists(rel); return `

    ${title}

    ${desc}

    ${ok ? "" : 'not yet written'}
    `; }) .join(""); const notes = (C.listDir("research/notes") || []) .filter((f) => f.name.endsWith(".md") && f.name !== "README.md") .map((f) => { const md = C.readMarkdown(f.rel); const doc = md && md.data.document ? md.data.document : f.name; return `

    ${R.esc(f.name.replace(".md", "").replace(/_/g, " "))}

    ${R.esc(doc)}

    `; }) .join(""); const body = `

    Research documents

    ${cards}

    Phase 1 literature notes

    ${notes || '

    No literature notes yet — Phase 1 has not started.

    '}

    The project charter itself is public: read the full research charter.

    `; page(res, { title: "Research", active: "Research", body }); }); // ---------------------------------------------------------------- markdown viewer app.get(/^\/doc\/(.+)$/, (req, res) => { const rel = req.params[0]; const md = rel.endsWith(".md") ? C.readMarkdown(rel) : null; if (!md) return notFound(res); const body = `

    Research / ${R.esc(rel)}

    ${metaChips(md.data)}
    ${R.markdownToHtml(md.content, rel)}
    `; page(res, { title: md.data.document || rel, active: "Research", body }); }); // ---------------------------------------------------------------- experiments app.get("/experiments", (req, res) => { const exps = C.listExperiments(); const cards = exps .map( (e) => `

    ${R.esc(e.id)}

    ${R.esc(e.purpose)}

    ${e.status} ${e.runs ? `${e.runs} result run${e.runs > 1 ? "s" : ""}` : ""}
    ` ) .join(""); const body = `

    Experiments

    Every experiment carries a registered hypothesis with an explicit falsification criterion (seven-field scientific block), mandatory controls and nulls, raw results, and an analysis. Negative results are kept. Micro-experiments A–H establish the noise floor, the correlational→causal survival rate, and the capture cost frontier before any large model is mapped.

    ${cards}
    `; page(res, { title: "Experiments", active: "Experiments", body }); }); app.get("/experiments/:id", (req, res) => { const exps = C.listExperiments(); const exp = exps.find((e) => e.id === req.params.id); if (!exp) return notFound(res); const sections = []; for (const [file, title] of [["hypothesis.md", "Hypothesis"], ["analysis.md", "Analysis"], ["README.md", "README"]]) { const md = C.readMarkdown(path.posix.join(exp.rel, file)); if (md && md.content.trim().length > 40) { sections.push(`

    ${title}

    ${metaChips(md.data)}
    ${R.markdownToHtml(md.content, path.posix.join(exp.rel, file))}
    `); } } const runs = C.listResultRuns().filter((r) => r.experiment === exp.id); const runsHtml = runs.length ? `

    Result runs

    ` : ""; const codeLink = C.exists(path.posix.join(exp.rel, "benchmark.py")) ? `

    View benchmark implementation (benchmark.py) →

    ` : ""; let charts = ""; if (exp.id === "expH_capture_cost_frontier") charts = expHCharts(); if (exp.id === "expA_probe_reliability") { charts = probeMapCharts(null, MAP_V2) + probeMapCharts(); } if (exp.id === "expC_causal_verification") charts = interventionsMapCharts(); if (charts) charts = `

    Result maps

    ${charts}
    `; const body = `

    Experiments / ${R.esc(exp.id)}

    ${R.esc(exp.id)}

    ${R.esc(exp.purpose)}

    ${codeLink}${charts}${sections.join("")}${runsHtml}`; page(res, { title: exp.id, active: "Experiments", body }); }); // ---------------------------------------------------------------- atlas app.get("/atlas", (req, res) => { const entries = C.listAtlasEntries(); const byModel = {}; for (const e of entries) (byModel[e.model] ||= []).push(e); const groups = Object.keys(byModel).sort().map((model) => { const cards = byModel[model] .map( (e) => `

    ${R.esc(e.mapType)} ${R.esc(e.version)}

    ${e.provenance && e.provenance.created ? "created " + R.esc(e.provenance.created) : ""}

    ${R.levelBadge(e.level)}
    ` ) .join(""); return `

    ${R.esc(model)}

    ${cards}
    `; }).join(""); const empty = `

    No maps published yet

    The atlas fills only after the mapping methodology exists (Phase 9) and each map clears its evidence bar. Every entry will carry a completed provenance.json (commit, config, model hash, hardware, dates) and a confidence.md stating its evidence level. Nothing is published that cannot be regenerated from committed code and versioned data.

    ${R.confidenceLadder()}
    `; const body = `

    Atlas

    Versioned map artifacts: atlas/<model>/<map type>/<version>. Only open-weight models are mapped, and no claim appears here above its evidence level.

    ${groups || empty}`; page(res, { title: "Atlas", active: "Atlas", body }); }); app.get("/atlas/:model/:mapType/:version", (req, res) => { const { model, mapType, version } = req.params; const entry = C.listAtlasEntries().find( (e) => e.model === model && e.mapType === mapType && e.version === version ); if (!entry) return notFound(res); const confidence = C.readMarkdown(path.posix.join(entry.rel, "confidence.md")); const prov = entry.provenance ? `
    ${R.highlightFile(JSON.stringify(entry.provenance, null, 2), "provenance.json")}
    ` : "

    provenance.json missing — this entry is not publishable.

    "; const files = entry.files .map((f) => `
  • ${f.name} ${(f.size / 1024).toFixed(1)} KiB
  • `) .join(""); const mapRel = path.posix.join(entry.rel, "map.json"); const mapCharts = !C.exists(mapRel) ? "" : mapType === "probes" ? probeMapCharts(null, mapRel) : mapType === "interventions" ? interventionsMapCharts(mapRel) : ""; const body = `

    Atlas / ${R.esc(entry.rel)}

    ${R.esc(model)} — ${R.esc(mapType)} ${R.esc(version)}

    ${R.levelBadge(entry.level)}

    ${mapCharts ? `

    The map

    ${mapCharts}
    ` : ""} ${confidence ? `

    Confidence

    ${R.markdownToHtml(confidence.content, entry.rel + "/confidence.md")}
    ` : ""}

    Provenance

    ${prov}

    Files

    `; page(res, { title: `${model}/${mapType}/${version}`, active: "Atlas", body }); }); // ---------------------------------------------------------------- results app.get("/results", (req, res) => { const runs = C.listResultRuns(); const rows = runs .map( (r) => `${r.experiment} ${r.timestamp} ${r.files.map((f) => `${f.name}`).join(" · ")}` ) .join(""); const body = `

    Raw results

    Every result is reproducible from commit hash + config + model hash + seed + hardware manifest, and each JSON embeds the manifest of the exact machine that produced it.

    ${rows || ''}
    ExperimentRun (UTC)Files
    No result runs yet.
    `; page(res, { title: "Results", active: "Results", body }); }); app.get(/^\/results\/(.+)$/, (req, res) => { const raw0 = req.params[0]; const rel = raw0.startsWith("results/") || raw0.startsWith("atlas/") ? raw0 : "results/" + raw0; const raw = C.readText(rel); if (raw === null) return notFound(res); let bodyContent; if (rel.endsWith(".json")) { const obj = C.readJson(rel); bodyContent = `
    ${R.highlightFile(JSON.stringify(obj, null, 2), "x.json")}
    `; } else { bodyContent = `
    ${R.esc(raw.slice(0, 200000))}
    `; } const body = `

    Results / ${R.esc(rel)}

    ${bodyContent}
    `; page(res, { title: rel, active: "Results", body }); }); // ---------------------------------------------------------------- code browser const CODE_ROOTS = ["src", "tools", "benchmarks", "experiments", "Makefile", "pyproject.toml", "CITATION.cff"]; app.get("/code", (req, res) => { const sections = CODE_ROOTS.map((root) => { if (!C.exists(root)) return ""; const st = C.listDir(root); if (st === null) { return `
  • ${root}
  • `; } const files = C.walk(root, (f) => C.isTextFile(f.rel) && !f.rel.includes("results/")); return `
  • ${root}/
  • `; }).join(""); const body = `

    Code

    The mapping library (src/modelmap/), tooling, benchmark harness, and experiment implementations. Every file carries the project's author header; MLX / PyTorch-MPS / Metal is the primary compute path — CUDA is never a core dependency.

    `; page(res, { title: "Code", active: "Code", body }); }); app.get(/^\/file\/(.+)$/, (req, res) => { const rel = req.params[0]; if (!C.isTextFile(rel)) return notFound(res); const raw = C.readText(rel); if (raw === null) return notFound(res); const name = rel.split("/").pop(); const body = `

    Code / ${R.esc(rel)}

    ${R.esc(rel)} ${raw.split("\n").length} lines
    ${R.highlightFile(raw.slice(0, 400000), name)}
    `; page(res, { title: name, active: "Code", body }); }); // ---------------------------------------------------------------- about app.get("/about", (req, res) => { const questions = [ ["Q1 — Localization", "Where inside a pretrained LLM do capabilities, knowledge domains, languages, and behaviors reside? Localized (layers, heads, neurons, weight blocks, directions) or diffuse?"], ["Q2 — Structure", "Which stable internal structures exist across inputs — circuits, feature directions, attention patterns, activation manifolds, weight-space geometry — and at what granularity are they real rather than artifacts of the probing method?"], ["Q3 — Comparability", "Can internal maps be compared across model sizes, checkpoints, quantization levels, and families? Is there a common coordinate system for model internals?"], ["Q4 — Cost", "Which mapping techniques are feasible on a 16–64 GB Mac, at which model sizes, and what is the accuracy/cost frontier of local interpretability?"], ["Q5 — Utility", "Do the maps predict anything useful — quantization sensitivity, pruning tolerance, working-set behavior, failure modes, editing targets?"], ]; const qHtml = questions .map(([q, d]) => `${q}${d}`) .join(""); const body = `

    About this project

    modelmap is an independent research project by Simon-Pierre Boucher (contact@spboucher.ai) building a systematic methodology — and the tooling behind it — to discover, measure, and map the internal structure of pretrained open-weight LLMs running locally on consumer Apple Silicon hardware, and publishing the resulting maps as a rigorous, reproducible public atlas on this site.

    The ideal outcome is not a gallery of suggestive visualizations. It is a reproducible cartographic standard for open-weight models — every map versioned, provenanced, confidence-labeled, and regenerable by anyone with a Mac — turning “we think the model does X” into “here is the map, its evidence level, and the script that rebuilds it.”

    Core research questions

    ${qHtml}

    The evidence standard

    ${R.confidenceLadder()}

    Every mapping technique runs with controls: shuffled labels for probes, random-direction baselines for steering, randomly-initialized-model baselines where meaningful, resample vs zero ablations. Structure is never claimed without showing the null. Replication across ≥3 seeds and ≥2 prompt sets, bootstrap confidence intervals, effect sizes, and multiple-comparison correction are mandatory for every published number.

    Target hardware

    Apple Silicon Mac (M1–M4 family), 16–64 GB unified memory, internal NVMe SSD, Metal GPU sharing memory with the CPU. Local interpretability on consumer hardware is itself an under-served niche: most tooling assumes CUDA clusters. Making rigorous mapping feasible on a Mac is part of the contribution. The stretch target is a full, causally-verified, versioned atlas of a 7B–14B model produced end-to-end on a single 32–64 GB Mac, comparable across ≥2 quantization levels and ≥2 model sizes.

    Sister project

    localvm-research investigates out-of-core LLM execution on the same hardware class — running models larger than memory. The projects cross-pollinate: modelmap's working-set and localization maps (Q5) feed localvm's execution decisions, and localvm's cost measurements calibrate modelmap's capture-feasibility frontier (Experiment H).

    Failure is an acceptable outcome — the charter defines explicit failure criteria, and the log records why an approach died, not just what survived.

    `; page(res, { title: "About", active: "About", body }); }); // ---------------------------------------------------------------- publications const FIGS = { "causal-band": () => interventionsMapCharts(), "probes-agreement": () => probeMapCharts("agreement", MAP_V2), "probes-arith": () => probeMapCharts("arith_valid", MAP_V2), "probes-word-order": () => probeMapCharts("word_order", MAP_V2), "probes-null": () => probeMapCharts("lang_id"), "storage": () => storageChart(), "capture": () => captureChart(), }; function listPublications() { return (C.listDir("publications") || []) .filter((f) => f.name.endsWith(".md")) .map((f) => { const md = C.readMarkdown(f.rel); return md ? { rel: f.rel, id: f.name.replace(".md", ""), data: md.data } : null; }) .filter(Boolean) .sort((a, b) => String(b.data.created).localeCompare(String(a.data.created))); } app.get("/publications", (req, res) => { const pubs = listPublications(); const cards = pubs.map((p) => `
    ${R.esc(p.data.pub_id || p.id)} ${R.esc(p.data.status || "draft")} ${R.esc(String(p.data.created))}

    ${R.esc(p.data.title || p.id)}

    ${R.esc(p.data.abstract ? String(p.data.abstract).slice(0, 300) + "…" : "")}

    Read the report →
    `).join(""); const body = `

    Publications

    Official technical reports of this project. Every number is regenerable from the stated commit; figures are rendered live from the same versioned results as the Atlas; negative results and publication-gate refusals are reported with the same prominence as positive findings.

    ${cards || '

    No publications yet.

    '}`; page(res, { title: "Publications", active: "Publications", body }); }); app.get("/publications/:id", (req, res) => { const rel = `publications/${req.params.id}.md`; const md = C.readMarkdown(rel); if (!md) return notFound(res); let html = R.markdownToHtml(md.content, rel); html = html.replace(/

    \{\{fig:([a-z0-9-]+)\}\}<\/p>/g, (_, key) => FIGS[key] ? FIGS[key]() : ""); const d = md.data; const body = `

    Publications / ${R.esc(d.pub_id || req.params.id)}

    ${R.esc(d.pub_id || req.params.id)} ${R.esc(d.status || "draft")} ${R.esc(String(d.created))}

    ${R.esc(d.title || req.params.id)}

    ${d.abstract ? `
    Abstract. ${R.esc(String(d.abstract))}
    ` : ""}
    ${html}
    ${d.cite ? `
    How to cite
    ${R.esc(String(d.cite))}
    ` : ""}
    `; page(res, { title: d.title || req.params.id, active: "Publications", body }); }); // ---------------------------------------------------------------- comments function commentsPage(res, { flash = "", flashKind = "ok" } = {}) { const items = Comments.list(); const listHtml = items.length ? `` : `

    No comments yet — be the first.

    `; const body = `

    Comments

    Questions, critiques, replication reports, pointers to related work — all welcome. Methodological challenges are especially valued: this project publishes its negative results, and a comment that breaks a map is a contribution.

    ${flash ? `
    ${R.esc(flash)}
    ` : ""}

    Leave a comment

    ${items.length ? items.length + " comment" + (items.length > 1 ? "s" : "") : "Comments"}

    ${listHtml}`; page(res, { title: "Comments", active: "Comments", body }); } app.get("/comments", (req, res) => { const flash = req.query.ok === "1" ? "Thank you — your comment is published." : req.query.err ? String(req.query.err) : ""; commentsPage(res, { flash, flashKind: req.query.ok === "1" ? "ok" : "err" }); }); app.post("/comments", (req, res) => { const out = Comments.add({ name: req.body.name, message: req.body.message, honeypot: req.body.website, ip: req.ip, }); if (out.ok) return res.redirect(303, "/comments?ok=1"); return res.redirect(303, "/comments?err=" + encodeURIComponent(out.error || "Could not post.")); }); // ---------------------------------------------------------------- misc app.get("/health", (req, res) => res.json({ ok: true, app: "modelmap-web", author: "Simon-Pierre Boucher", website: "https://modelmap.io" })); function notFound(res) { res.status(404); page(res, { title: "Not found", active: "", body: `

    404

    That page does not exist. Back to home.

    ` }); } app.use((req, res) => notFound(res)); app.listen(PORT, () => console.log(`modelmap-web listening on :${PORT}`));