Web platform: research showcase (English, light theme) in web/
Express app serving the full paper trail from a content snapshot: home (stat tiles + expH SSD chart, validated palette), research docs, experiments with hypotheses/analyses, raw results, code browser, about. sync-content.sh snapshots the repo + build-info.json. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Showing 9 changed files with +1,947 and −0
modified
.gitignore
+4 −0
@@ -44,3 +44,7 @@ models_cache/ | ||
| 44 | 44 | # Raw traces can be huge; committed results are curated JSON/CSV/plots |
| 45 | 45 | results/**/*.trace |
| 46 | 46 | results/**/*.fsusage |
| 47 | + | |
| 48 | +# web platform: generated content snapshot + deps | |
| 49 | +web/content/ | |
| 50 | +web/node_modules/ | |
added
web/lib/content.js
+208 −0
@@ -0,0 +1,208 @@ | ||
| 1 | +// ============================================================================ | |
| 2 | +// Project : localvm-research | |
| 3 | +// File : web/lib/content.js | |
| 4 | +// Purpose : Content index over the synced research-repo snapshot (content/) | |
| 5 | +// Author : Simon-Pierre Boucher | |
| 6 | +// Contact : contact@spboucher.ai | |
| 7 | +// Created : 2026-08-12 | |
| 8 | +// Modified : 2026-08-12 | |
| 9 | +// Platform : macOS / Apple Silicon (arm64) — Node.js | |
| 10 | +// License : All rights reserved (research code) | |
| 11 | +// ============================================================================ | |
| 12 | +"use strict"; | |
| 13 | + | |
| 14 | +const fs = require("fs"); | |
| 15 | +const path = require("path"); | |
| 16 | +const matter = require("gray-matter"); | |
| 17 | + | |
| 18 | +const CONTENT_ROOT = path.join(__dirname, "..", "content"); | |
| 19 | + | |
| 20 | +const TEXT_EXT = new Set([ | |
| 21 | + ".md", ".py", ".sh", ".toml", ".yaml", ".yml", ".cff", ".json", | |
| 22 | + ".js", ".css", ".metal", ".cpp", ".h", ".hpp", ".swift", ".m", ".txt", ".log", | |
| 23 | +]); | |
| 24 | + | |
| 25 | +/** Resolve a repo-relative path safely inside content/, or return null. */ | |
| 26 | +function safeResolve(rel) { | |
| 27 | + const abs = path.resolve(CONTENT_ROOT, rel); | |
| 28 | + if (!abs.startsWith(CONTENT_ROOT + path.sep) && abs !== CONTENT_ROOT) return null; | |
| 29 | + return abs; | |
| 30 | +} | |
| 31 | + | |
| 32 | +function exists(rel) { | |
| 33 | + const abs = safeResolve(rel); | |
| 34 | + return abs !== null && fs.existsSync(abs); | |
| 35 | +} | |
| 36 | + | |
| 37 | +function readText(rel) { | |
| 38 | + const abs = safeResolve(rel); | |
| 39 | + if (!abs || !fs.existsSync(abs) || !fs.statSync(abs).isFile()) return null; | |
| 40 | + return fs.readFileSync(abs, "utf8"); | |
| 41 | +} | |
| 42 | + | |
| 43 | +/** Read a markdown file, returning { data (front matter), content }. */ | |
| 44 | +function readMarkdown(rel) { | |
| 45 | + const raw = readText(rel); | |
| 46 | + if (raw === null) return null; | |
| 47 | + try { | |
| 48 | + return matter(raw); | |
| 49 | + } catch { | |
| 50 | + return { data: {}, content: raw }; | |
| 51 | + } | |
| 52 | +} | |
| 53 | + | |
| 54 | +/** List a directory inside content/: [{name, rel, dir, size}] */ | |
| 55 | +function listDir(rel) { | |
| 56 | + const abs = safeResolve(rel || "."); | |
| 57 | + if (!abs || !fs.existsSync(abs) || !fs.statSync(abs).isDirectory()) return null; | |
| 58 | + return fs | |
| 59 | + .readdirSync(abs) | |
| 60 | + .filter((n) => !n.startsWith(".") && n !== "node_modules") | |
| 61 | + .map((n) => { | |
| 62 | + const st = fs.statSync(path.join(abs, n)); | |
| 63 | + return { | |
| 64 | + name: n, | |
| 65 | + rel: path.posix.join(rel || "", n), | |
| 66 | + dir: st.isDirectory(), | |
| 67 | + size: st.size, | |
| 68 | + }; | |
| 69 | + }) | |
| 70 | + .sort((a, b) => (a.dir !== b.dir ? (a.dir ? -1 : 1) : a.name.localeCompare(b.name))); | |
| 71 | +} | |
| 72 | + | |
| 73 | +/** Recursively collect files under rel matching a predicate. */ | |
| 74 | +function walk(rel, predicate, out = []) { | |
| 75 | + const entries = listDir(rel); | |
| 76 | + if (!entries) return out; | |
| 77 | + for (const e of entries) { | |
| 78 | + if (e.dir) walk(e.rel, predicate, out); | |
| 79 | + else if (predicate(e)) out.push(e); | |
| 80 | + } | |
| 81 | + return out; | |
| 82 | +} | |
| 83 | + | |
| 84 | +function isTextFile(rel) { | |
| 85 | + return TEXT_EXT.has(path.extname(rel).toLowerCase()) || path.basename(rel) === "Makefile"; | |
| 86 | +} | |
| 87 | + | |
| 88 | +/** Parse research/LOG.md into entries [{title, body}] (## headings). */ | |
| 89 | +function parseLogEntries() { | |
| 90 | + const md = readMarkdown("research/LOG.md"); | |
| 91 | + if (!md) return []; | |
| 92 | + const parts = md.content.split(/^## /m).slice(1); | |
| 93 | + return parts.map((p) => { | |
| 94 | + const nl = p.indexOf("\n"); | |
| 95 | + return { title: p.slice(0, nl).trim(), body: p.slice(nl + 1).replace(/^---\s*$/m, "").trim() }; | |
| 96 | + }); | |
| 97 | +} | |
| 98 | + | |
| 99 | +/** Experiment cards: micro experiments + candidates. */ | |
| 100 | +function listExperiments() { | |
| 101 | + const out = []; | |
| 102 | + for (const base of ["experiments/micro", "experiments"]) { | |
| 103 | + const entries = listDir(base) || []; | |
| 104 | + for (const e of entries) { | |
| 105 | + if (!e.dir || e.name === "micro") continue; | |
| 106 | + const readme = readMarkdown(path.posix.join(e.rel, "README.md")); | |
| 107 | + const hasAnalysis = (readText(path.posix.join(e.rel, "analysis.md")) || "").length > 400; | |
| 108 | + const runs = listResultRuns().filter((r) => r.experiment === e.name); | |
| 109 | + let purpose = ""; | |
| 110 | + if (readme) { | |
| 111 | + const line = readme.content.split("\n").find((l) => l.trim() && !l.startsWith("#")); | |
| 112 | + purpose = line ? line.trim() : ""; | |
| 113 | + } | |
| 114 | + out.push({ | |
| 115 | + id: e.name, | |
| 116 | + rel: e.rel, | |
| 117 | + purpose, | |
| 118 | + status: runs.length > 0 && hasAnalysis ? "completed" : runs.length > 0 ? "has results" : "scaffolded", | |
| 119 | + runs: runs.length, | |
| 120 | + }); | |
| 121 | + } | |
| 122 | + } | |
| 123 | + return out; | |
| 124 | +} | |
| 125 | + | |
| 126 | +/** Result runs: results/<experiment>/<timestamp>/*.json */ | |
| 127 | +function listResultRuns() { | |
| 128 | + const out = []; | |
| 129 | + for (const exp of listDir("results") || []) { | |
| 130 | + if (!exp.dir) continue; | |
| 131 | + for (const run of listDir(exp.rel) || []) { | |
| 132 | + if (!run.dir) continue; | |
| 133 | + const files = (listDir(run.rel) || []).filter((f) => !f.dir); | |
| 134 | + out.push({ experiment: exp.name, timestamp: run.name, rel: run.rel, files }); | |
| 135 | + } | |
| 136 | + } | |
| 137 | + return out.sort((a, b) => b.timestamp.localeCompare(a.timestamp)); | |
| 138 | +} | |
| 139 | + | |
| 140 | +function readJson(rel) { | |
| 141 | + const raw = readText(rel); | |
| 142 | + if (raw === null) return null; | |
| 143 | + try { | |
| 144 | + return JSON.parse(raw); | |
| 145 | + } catch { | |
| 146 | + return null; | |
| 147 | + } | |
| 148 | +} | |
| 149 | + | |
| 150 | +/** Build info written by sync-content.sh (commit, counts, date). */ | |
| 151 | +function buildInfo() { | |
| 152 | + return readJson("build-info.json") || {}; | |
| 153 | +} | |
| 154 | + | |
| 155 | +/** Site-wide stats for the home page. */ | |
| 156 | +function stats() { | |
| 157 | + const bib = readText("research/bibliography.md") || ""; | |
| 158 | + const sources = bib.split("\n").filter((l) => l.trim().startsWith("- ")).length; | |
| 159 | + const gapsDoc = readText("research/research_gaps.md") || ""; | |
| 160 | + const gaps = (gapsDoc.match(/^#{2,3}\s+G\d{2}/gm) || []).length; | |
| 161 | + const experiments = listExperiments(); | |
| 162 | + const notes = (listDir("research/notes") || []).filter((f) => f.name.endsWith(".md") && f.name !== "README.md").length; | |
| 163 | + return { | |
| 164 | + sources, | |
| 165 | + gaps, | |
| 166 | + notes, | |
| 167 | + experiments: experiments.length, | |
| 168 | + experimentsDone: experiments.filter((e) => e.status === "completed").length, | |
| 169 | + resultRuns: listResultRuns().length, | |
| 170 | + logEntries: parseLogEntries().length, | |
| 171 | + build: buildInfo(), | |
| 172 | + }; | |
| 173 | +} | |
| 174 | + | |
| 175 | +/** Latest expH results for the home-page chart: cold random cells. */ | |
| 176 | +function expHChartData() { | |
| 177 | + const runs = listResultRuns().filter((r) => r.experiment === "expH_ssd_feasibility"); | |
| 178 | + if (!runs.length) return null; | |
| 179 | + const data = readJson(path.posix.join(runs[0].rel, "results.json")); | |
| 180 | + if (!data) return null; | |
| 181 | + const cells = (data.cells || []).filter( | |
| 182 | + (c) => c.nocache && !c.gpu_load && c.pattern === "random" && c.block_bytes <= 1 << 20 | |
| 183 | + ); | |
| 184 | + const series = {}; | |
| 185 | + for (const c of cells) { | |
| 186 | + (series[c.threads] ||= []).push({ block: c.block_bytes, mbps: c.mb_per_s_mean }); | |
| 187 | + } | |
| 188 | + for (const k of Object.keys(series)) series[k].sort((a, b) => a.block - b.block); | |
| 189 | + return { series, run: runs[0].timestamp, manifest: data.manifest || {} }; | |
| 190 | +} | |
| 191 | + | |
| 192 | +module.exports = { | |
| 193 | + CONTENT_ROOT, | |
| 194 | + safeResolve, | |
| 195 | + exists, | |
| 196 | + readText, | |
| 197 | + readMarkdown, | |
| 198 | + readJson, | |
| 199 | + listDir, | |
| 200 | + walk, | |
| 201 | + isTextFile, | |
| 202 | + parseLogEntries, | |
| 203 | + listExperiments, | |
| 204 | + listResultRuns, | |
| 205 | + buildInfo, | |
| 206 | + stats, | |
| 207 | + expHChartData, | |
| 208 | +}; | |
added
web/lib/render.js
+163 −0
@@ -0,0 +1,163 @@ | ||
| 1 | +// ============================================================================ | |
| 2 | +// Project : localvm-research | |
| 3 | +// File : web/lib/render.js | |
| 4 | +// Purpose : HTML layout, markdown/code rendering, SVG chart for the platform | |
| 5 | +// Author : Simon-Pierre Boucher | |
| 6 | +// Contact : contact@spboucher.ai | |
| 7 | +// Created : 2026-08-12 | |
| 8 | +// Modified : 2026-08-12 | |
| 9 | +// Platform : macOS / Apple Silicon (arm64) — Node.js | |
| 10 | +// License : All rights reserved (research code) | |
| 11 | +// ============================================================================ | |
| 12 | +"use strict"; | |
| 13 | + | |
| 14 | +const { marked } = require("marked"); | |
| 15 | +const hljs = require("highlight.js"); | |
| 16 | + | |
| 17 | +function esc(s) { | |
| 18 | + return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); | |
| 19 | +} | |
| 20 | + | |
| 21 | +// Rewrite relative markdown links to platform routes; highlight code blocks. | |
| 22 | +const renderer = new marked.Renderer(); | |
| 23 | +renderer.code = function ({ text, lang }) { | |
| 24 | + let html; | |
| 25 | + if (lang && hljs.getLanguage(lang)) { | |
| 26 | + html = hljs.highlight(text, { language: lang }).value; | |
| 27 | + } else { | |
| 28 | + html = esc(text); | |
| 29 | + } | |
| 30 | + return `<pre class="codeblock"><code class="hljs">${html}</code></pre>`; | |
| 31 | +}; | |
| 32 | + | |
| 33 | +function markdownToHtml(md, baseRel) { | |
| 34 | + marked.use({ | |
| 35 | + renderer, | |
| 36 | + walkTokens(token) { | |
| 37 | + if (token.type === "link" && token.href && !/^(https?:|mailto:|#|\/)/.test(token.href)) { | |
| 38 | + const base = baseRel.includes("/") ? baseRel.slice(0, baseRel.lastIndexOf("/")) : ""; | |
| 39 | + const joined = (base ? base + "/" : "") + token.href; | |
| 40 | + const norm = joined.split("/").reduce((acc, part) => { | |
| 41 | + if (part === "..") acc.pop(); | |
| 42 | + else if (part !== "." && part !== "") acc.push(part); | |
| 43 | + return acc; | |
| 44 | + }, []).join("/"); | |
| 45 | + token.href = norm.endsWith(".md") ? "/doc/" + norm : "/file/" + norm; | |
| 46 | + } | |
| 47 | + }, | |
| 48 | + }); | |
| 49 | + return marked.parse(md); | |
| 50 | +} | |
| 51 | + | |
| 52 | +function highlightFile(text, filename) { | |
| 53 | + const ext = filename.slice(filename.lastIndexOf(".") + 1).toLowerCase(); | |
| 54 | + const langMap = { | |
| 55 | + py: "python", sh: "bash", js: "javascript", css: "css", json: "json", | |
| 56 | + toml: "ini", yaml: "yaml", yml: "yaml", cff: "yaml", md: "markdown", | |
| 57 | + metal: "cpp", cpp: "cpp", h: "cpp", hpp: "cpp", swift: "swift", m: "objectivec", | |
| 58 | + }; | |
| 59 | + const lang = filename === "Makefile" ? "makefile" : langMap[ext]; | |
| 60 | + if (lang && hljs.getLanguage(lang)) return hljs.highlight(text, { language: lang }).value; | |
| 61 | + return esc(text); | |
| 62 | +} | |
| 63 | + | |
| 64 | +const NAV = [ | |
| 65 | + ["/", "Home"], | |
| 66 | + ["/research", "Research"], | |
| 67 | + ["/experiments", "Experiments"], | |
| 68 | + ["/results", "Results"], | |
| 69 | + ["/code", "Code"], | |
| 70 | + ["/doc/research/bibliography.md", "Bibliography"], | |
| 71 | + ["/about", "About"], | |
| 72 | +]; | |
| 73 | + | |
| 74 | +function layout({ title, active, body, buildInfo = {} }) { | |
| 75 | + const nav = NAV.map( | |
| 76 | + ([href, label]) => | |
| 77 | + `<a href="${href}" class="${active === label ? "active" : ""}">${label}</a>` | |
| 78 | + ).join(""); | |
| 79 | + const commit = buildInfo.commit ? buildInfo.commit.slice(0, 7) : "dev"; | |
| 80 | + const synced = buildInfo.generated ? buildInfo.generated.slice(0, 10) : ""; | |
| 81 | + return `<!DOCTYPE html> | |
| 82 | +<html lang="en"> | |
| 83 | +<head> | |
| 84 | +<meta charset="utf-8"> | |
| 85 | +<meta name="viewport" content="width=device-width, initial-scale=1"> | |
| 86 | +<title>${esc(title)} · localvm-research</title> | |
| 87 | +<meta name="description" content="localvm-research — running LLMs larger than memory on consumer Apple Silicon. Research by Simon-Pierre Boucher."> | |
| 88 | +<link rel="stylesheet" href="/static/style.css"> | |
| 89 | +<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='0.9em' font-size='90'>🧠</text></svg>"> | |
| 90 | +</head> | |
| 91 | +<body> | |
| 92 | +<header class="site-header"> | |
| 93 | + <div class="wrap header-row"> | |
| 94 | + <a class="brand" href="/">localvm<span class="brand-dim">-research</span></a> | |
| 95 | + <nav class="site-nav">${nav}</nav> | |
| 96 | + </div> | |
| 97 | +</header> | |
| 98 | +<main class="wrap">${body}</main> | |
| 99 | +<footer class="site-footer"> | |
| 100 | + <div class="wrap"> | |
| 101 | + <span>© 2026 Simon-Pierre Boucher — <a href="mailto:contact@spboucher.ai">contact@spboucher.ai</a> · All rights reserved (research code)</span> | |
| 102 | + <span class="footer-meta">snapshot ${esc(commit)}${synced ? " · synced " + esc(synced) : ""} · Apple M5 Max / 48 GB / macOS 27</span> | |
| 103 | + </div> | |
| 104 | +</footer> | |
| 105 | +<script src="/static/app.js" defer></script> | |
| 106 | +</body> | |
| 107 | +</html>`; | |
| 108 | +} | |
| 109 | + | |
| 110 | +/** | |
| 111 | + * SSD throughput line chart (log-y), server-rendered SVG. | |
| 112 | + * Form: change-of-magnitude across an ordered log2 x (block size), 3 series (QD). | |
| 113 | + * Palette: validated categorical slots 1–3 (see dataviz reference palette); | |
| 114 | + * relief for the aqua WARN is provided by direct labels at line ends. | |
| 115 | + */ | |
| 116 | +function ssdChartSvg(chart) { | |
| 117 | + if (!chart) return ""; | |
| 118 | + const SERIES_COLORS = { 1: "#2a78d6", 4: "#eb6834", 8: "#1baf7a" }; | |
| 119 | + const W = 720, H = 340, ML = 64, MR = 96, MT = 18, MB = 44; | |
| 120 | + const iw = W - ML - MR, ih = H - MT - MB; | |
| 121 | + const blocks = [4096, 16384, 65536, 262144, 1048576]; | |
| 122 | + const xPos = (b) => ML + (Math.log2(b) - 12) / (20 - 12) * iw; | |
| 123 | + const yMin = Math.log10(50), yMax = Math.log10(20000); | |
| 124 | + const yPos = (v) => MT + ih - ((Math.log10(v) - yMin) / (yMax - yMin)) * ih; | |
| 125 | + | |
| 126 | + const gridVals = [100, 1000, 10000]; | |
| 127 | + let g = ""; | |
| 128 | + for (const v of gridVals) { | |
| 129 | + g += `<line x1="${ML}" y1="${yPos(v)}" x2="${ML + iw}" y2="${yPos(v)}" class="grid"/>` + | |
| 130 | + `<text x="${ML - 8}" y="${yPos(v) + 4}" class="tick" text-anchor="end">${v >= 1000 ? v / 1000 + " GB/s" : v + " MB/s"}</text>`; | |
| 131 | + } | |
| 132 | + let xticks = ""; | |
| 133 | + for (const b of blocks) { | |
| 134 | + const lbl = b >= 1048576 ? "1 MiB" : b >= 1024 ? b / 1024 + " KiB" : b + " B"; | |
| 135 | + xticks += `<text x="${xPos(b)}" y="${MT + ih + 20}" class="tick" text-anchor="middle">${lbl}</text>`; | |
| 136 | + } | |
| 137 | + let lines = "", dots = "", labels = ""; | |
| 138 | + const qds = Object.keys(chart.series).map(Number).sort((a, b) => a - b); | |
| 139 | + for (const qd of qds) { | |
| 140 | + const pts = chart.series[qd].filter((p) => blocks.includes(p.block)); | |
| 141 | + if (!pts.length) continue; | |
| 142 | + const c = SERIES_COLORS[qd] || "#2a78d6"; | |
| 143 | + const d = pts.map((p, i) => `${i ? "L" : "M"}${xPos(p.block).toFixed(1)},${yPos(p.mbps).toFixed(1)}`).join(" "); | |
| 144 | + lines += `<path d="${d}" fill="none" stroke="${c}" stroke-width="2" stroke-linejoin="round"/>`; | |
| 145 | + for (const p of pts) { | |
| 146 | + const val = p.mbps >= 1000 ? (p.mbps / 1000).toFixed(1) + " GB/s" : Math.round(p.mbps) + " MB/s"; | |
| 147 | + dots += `<circle cx="${xPos(p.block).toFixed(1)}" cy="${yPos(p.mbps).toFixed(1)}" r="4.5" fill="${c}" stroke="#fcfcfb" stroke-width="2" class="pt" data-tip="QD${qd} · ${p.block >= 1048576 ? "1 MiB" : p.block / 1024 + " KiB"} random (cold): ${val}"/>`; | |
| 148 | + } | |
| 149 | + const last = pts[pts.length - 1]; | |
| 150 | + labels += `<text x="${xPos(last.block) + 12}" y="${yPos(last.mbps) + 4}" class="series-label" fill="${c}">QD ${qd}</text>`; | |
| 151 | + } | |
| 152 | + return `<figure class="chart-fig"> | |
| 153 | +<svg viewBox="0 0 ${W} ${H}" role="img" aria-label="Cold random SSD read throughput by block size and queue depth, log scale"> | |
| 154 | +<line x1="${ML}" y1="${MT + ih}" x2="${ML + iw}" y2="${MT + ih}" class="axis"/> | |
| 155 | +${g}${xticks}${lines}${dots}${labels} | |
| 156 | +<text x="${ML + iw / 2}" y="${H - 6}" class="axis-title" text-anchor="middle">read block size (random, F_NOCACHE cold, log–log)</text> | |
| 157 | +</svg> | |
| 158 | +<div id="chart-tip" class="chart-tip" hidden></div> | |
| 159 | +<figcaption>Measured on this project's M5 Max (expH, run ${esc(chart.run)}, 3 repeats/cell, iostat-validated ceiling ≈ 13.1 GB/s). Lines: reader queue depth.</figcaption> | |
| 160 | +</figure>`; | |
| 161 | +} | |
| 162 | + | |
| 163 | +module.exports = { esc, markdownToHtml, highlightFile, layout, ssdChartSvg }; | |
added
web/package-lock.json
+961 −0
@@ -0,0 +1,961 @@ | ||
| 1 | +{ | |
| 2 | + "name": "localvm-web", | |
| 3 | + "version": "1.0.0", | |
| 4 | + "lockfileVersion": 3, | |
| 5 | + "requires": true, | |
| 6 | + "packages": { | |
| 7 | + "": { | |
| 8 | + "name": "localvm-web", | |
| 9 | + "version": "1.0.0", | |
| 10 | + "license": "UNLICENSED", | |
| 11 | + "dependencies": { | |
| 12 | + "express": "^4.21.0", | |
| 13 | + "gray-matter": "^4.0.3", | |
| 14 | + "highlight.js": "^11.10.0", | |
| 15 | + "marked": "^14.1.0" | |
| 16 | + } | |
| 17 | + }, | |
| 18 | + "node_modules/accepts": { | |
| 19 | + "version": "1.3.8", | |
| 20 | + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", | |
| 21 | + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", | |
| 22 | + "license": "MIT", | |
| 23 | + "dependencies": { | |
| 24 | + "mime-types": "~2.1.34", | |
| 25 | + "negotiator": "0.6.3" | |
| 26 | + }, | |
| 27 | + "engines": { | |
| 28 | + "node": ">= 0.6" | |
| 29 | + } | |
| 30 | + }, | |
| 31 | + "node_modules/argparse": { | |
| 32 | + "version": "1.0.10", | |
| 33 | + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", | |
| 34 | + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", | |
| 35 | + "license": "MIT", | |
| 36 | + "dependencies": { | |
| 37 | + "sprintf-js": "~1.0.2" | |
| 38 | + } | |
| 39 | + }, | |
| 40 | + "node_modules/array-flatten": { | |
| 41 | + "version": "1.1.1", | |
| 42 | + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", | |
| 43 | + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", | |
| 44 | + "license": "MIT" | |
| 45 | + }, | |
| 46 | + "node_modules/body-parser": { | |
| 47 | + "version": "1.20.6", | |
| 48 | + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", | |
| 49 | + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", | |
| 50 | + "license": "MIT", | |
| 51 | + "dependencies": { | |
| 52 | + "bytes": "~3.1.2", | |
| 53 | + "content-type": "~1.0.5", | |
| 54 | + "debug": "2.6.9", | |
| 55 | + "depd": "2.0.0", | |
| 56 | + "destroy": "~1.2.0", | |
| 57 | + "http-errors": "~2.0.1", | |
| 58 | + "iconv-lite": "~0.4.24", | |
| 59 | + "on-finished": "~2.4.1", | |
| 60 | + "qs": "~6.15.1", | |
| 61 | + "raw-body": "~2.5.3", | |
| 62 | + "type-is": "~1.6.18", | |
| 63 | + "unpipe": "~1.0.0" | |
| 64 | + }, | |
| 65 | + "engines": { | |
| 66 | + "node": ">= 0.8", | |
| 67 | + "npm": "1.2.8000 || >= 1.4.16" | |
| 68 | + } | |
| 69 | + }, | |
| 70 | + "node_modules/bytes": { | |
| 71 | + "version": "3.1.2", | |
| 72 | + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", | |
| 73 | + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", | |
| 74 | + "license": "MIT", | |
| 75 | + "engines": { | |
| 76 | + "node": ">= 0.8" | |
| 77 | + } | |
| 78 | + }, | |
| 79 | + "node_modules/call-bind-apply-helpers": { | |
| 80 | + "version": "1.0.2", | |
| 81 | + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", | |
| 82 | + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", | |
| 83 | + "license": "MIT", | |
| 84 | + "dependencies": { | |
| 85 | + "es-errors": "^1.3.0", | |
| 86 | + "function-bind": "^1.1.2" | |
| 87 | + }, | |
| 88 | + "engines": { | |
| 89 | + "node": ">= 0.4" | |
| 90 | + } | |
| 91 | + }, | |
| 92 | + "node_modules/call-bound": { | |
| 93 | + "version": "1.0.4", | |
| 94 | + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", | |
| 95 | + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", | |
| 96 | + "license": "MIT", | |
| 97 | + "dependencies": { | |
| 98 | + "call-bind-apply-helpers": "^1.0.2", | |
| 99 | + "get-intrinsic": "^1.3.0" | |
| 100 | + }, | |
| 101 | + "engines": { | |
| 102 | + "node": ">= 0.4" | |
| 103 | + }, | |
| 104 | + "funding": { | |
| 105 | + "url": "https://github.com/sponsors/ljharb" | |
| 106 | + } | |
| 107 | + }, | |
| 108 | + "node_modules/content-disposition": { | |
| 109 | + "version": "0.5.4", | |
| 110 | + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", | |
| 111 | + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", | |
| 112 | + "license": "MIT", | |
| 113 | + "dependencies": { | |
| 114 | + "safe-buffer": "5.2.1" | |
| 115 | + }, | |
| 116 | + "engines": { | |
| 117 | + "node": ">= 0.6" | |
| 118 | + } | |
| 119 | + }, | |
| 120 | + "node_modules/content-type": { | |
| 121 | + "version": "1.0.5", | |
| 122 | + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", | |
| 123 | + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", | |
| 124 | + "license": "MIT", | |
| 125 | + "engines": { | |
| 126 | + "node": ">= 0.6" | |
| 127 | + } | |
| 128 | + }, | |
| 129 | + "node_modules/cookie": { | |
| 130 | + "version": "0.7.2", | |
| 131 | + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", | |
| 132 | + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", | |
| 133 | + "license": "MIT", | |
| 134 | + "engines": { | |
| 135 | + "node": ">= 0.6" | |
| 136 | + } | |
| 137 | + }, | |
| 138 | + "node_modules/cookie-signature": { | |
| 139 | + "version": "1.0.7", | |
| 140 | + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", | |
| 141 | + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", | |
| 142 | + "license": "MIT" | |
| 143 | + }, | |
| 144 | + "node_modules/debug": { | |
| 145 | + "version": "2.6.9", | |
| 146 | + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", | |
| 147 | + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", | |
| 148 | + "license": "MIT", | |
| 149 | + "dependencies": { | |
| 150 | + "ms": "2.0.0" | |
| 151 | + } | |
| 152 | + }, | |
| 153 | + "node_modules/depd": { | |
| 154 | + "version": "2.0.0", | |
| 155 | + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", | |
| 156 | + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", | |
| 157 | + "license": "MIT", | |
| 158 | + "engines": { | |
| 159 | + "node": ">= 0.8" | |
| 160 | + } | |
| 161 | + }, | |
| 162 | + "node_modules/destroy": { | |
| 163 | + "version": "1.2.0", | |
| 164 | + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", | |
| 165 | + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", | |
| 166 | + "license": "MIT", | |
| 167 | + "engines": { | |
| 168 | + "node": ">= 0.8", | |
| 169 | + "npm": "1.2.8000 || >= 1.4.16" | |
| 170 | + } | |
| 171 | + }, | |
| 172 | + "node_modules/dunder-proto": { | |
| 173 | + "version": "1.0.1", | |
| 174 | + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", | |
| 175 | + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", | |
| 176 | + "license": "MIT", | |
| 177 | + "dependencies": { | |
| 178 | + "call-bind-apply-helpers": "^1.0.1", | |
| 179 | + "es-errors": "^1.3.0", | |
| 180 | + "gopd": "^1.2.0" | |
| 181 | + }, | |
| 182 | + "engines": { | |
| 183 | + "node": ">= 0.4" | |
| 184 | + } | |
| 185 | + }, | |
| 186 | + "node_modules/ee-first": { | |
| 187 | + "version": "1.1.1", | |
| 188 | + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", | |
| 189 | + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", | |
| 190 | + "license": "MIT" | |
| 191 | + }, | |
| 192 | + "node_modules/encodeurl": { | |
| 193 | + "version": "2.0.0", | |
| 194 | + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", | |
| 195 | + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", | |
| 196 | + "license": "MIT", | |
| 197 | + "engines": { | |
| 198 | + "node": ">= 0.8" | |
| 199 | + } | |
| 200 | + }, | |
| 201 | + "node_modules/es-define-property": { | |
| 202 | + "version": "1.0.1", | |
| 203 | + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", | |
| 204 | + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", | |
| 205 | + "license": "MIT", | |
| 206 | + "engines": { | |
| 207 | + "node": ">= 0.4" | |
| 208 | + } | |
| 209 | + }, | |
| 210 | + "node_modules/es-errors": { | |
| 211 | + "version": "1.3.0", | |
| 212 | + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", | |
| 213 | + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", | |
| 214 | + "license": "MIT", | |
| 215 | + "engines": { | |
| 216 | + "node": ">= 0.4" | |
| 217 | + } | |
| 218 | + }, | |
| 219 | + "node_modules/es-object-atoms": { | |
| 220 | + "version": "1.1.2", | |
| 221 | + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", | |
| 222 | + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", | |
| 223 | + "license": "MIT", | |
| 224 | + "dependencies": { | |
| 225 | + "es-errors": "^1.3.0" | |
| 226 | + }, | |
| 227 | + "engines": { | |
| 228 | + "node": ">= 0.4" | |
| 229 | + } | |
| 230 | + }, | |
| 231 | + "node_modules/escape-html": { | |
| 232 | + "version": "1.0.3", | |
| 233 | + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", | |
| 234 | + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", | |
| 235 | + "license": "MIT" | |
| 236 | + }, | |
| 237 | + "node_modules/esprima": { | |
| 238 | + "version": "4.0.1", | |
| 239 | + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", | |
| 240 | + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", | |
| 241 | + "license": "BSD-2-Clause", | |
| 242 | + "bin": { | |
| 243 | + "esparse": "bin/esparse.js", | |
| 244 | + "esvalidate": "bin/esvalidate.js" | |
| 245 | + }, | |
| 246 | + "engines": { | |
| 247 | + "node": ">=4" | |
| 248 | + } | |
| 249 | + }, | |
| 250 | + "node_modules/etag": { | |
| 251 | + "version": "1.8.1", | |
| 252 | + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", | |
| 253 | + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", | |
| 254 | + "license": "MIT", | |
| 255 | + "engines": { | |
| 256 | + "node": ">= 0.6" | |
| 257 | + } | |
| 258 | + }, | |
| 259 | + "node_modules/express": { | |
| 260 | + "version": "4.22.2", | |
| 261 | + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", | |
| 262 | + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", | |
| 263 | + "license": "MIT", | |
| 264 | + "dependencies": { | |
| 265 | + "accepts": "~1.3.8", | |
| 266 | + "array-flatten": "1.1.1", | |
| 267 | + "body-parser": "~1.20.5", | |
| 268 | + "content-disposition": "~0.5.4", | |
| 269 | + "content-type": "~1.0.4", | |
| 270 | + "cookie": "~0.7.1", | |
| 271 | + "cookie-signature": "~1.0.6", | |
| 272 | + "debug": "2.6.9", | |
| 273 | + "depd": "2.0.0", | |
| 274 | + "encodeurl": "~2.0.0", | |
| 275 | + "escape-html": "~1.0.3", | |
| 276 | + "etag": "~1.8.1", | |
| 277 | + "finalhandler": "~1.3.1", | |
| 278 | + "fresh": "~0.5.2", | |
| 279 | + "http-errors": "~2.0.0", | |
| 280 | + "merge-descriptors": "1.0.3", | |
| 281 | + "methods": "~1.1.2", | |
| 282 | + "on-finished": "~2.4.1", | |
| 283 | + "parseurl": "~1.3.3", | |
| 284 | + "path-to-regexp": "~0.1.12", | |
| 285 | + "proxy-addr": "~2.0.7", | |
| 286 | + "qs": "~6.15.1", | |
| 287 | + "range-parser": "~1.2.1", | |
| 288 | + "safe-buffer": "5.2.1", | |
| 289 | + "send": "~0.19.0", | |
| 290 | + "serve-static": "~1.16.2", | |
| 291 | + "setprototypeof": "1.2.0", | |
| 292 | + "statuses": "~2.0.1", | |
| 293 | + "type-is": "~1.6.18", | |
| 294 | + "utils-merge": "1.0.1", | |
| 295 | + "vary": "~1.1.2" | |
| 296 | + }, | |
| 297 | + "engines": { | |
| 298 | + "node": ">= 0.10.0" | |
| 299 | + }, | |
| 300 | + "funding": { | |
| 301 | + "type": "opencollective", | |
| 302 | + "url": "https://opencollective.com/express" | |
| 303 | + } | |
| 304 | + }, | |
| 305 | + "node_modules/extend-shallow": { | |
| 306 | + "version": "2.0.1", | |
| 307 | + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", | |
| 308 | + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", | |
| 309 | + "license": "MIT", | |
| 310 | + "dependencies": { | |
| 311 | + "is-extendable": "^0.1.0" | |
| 312 | + }, | |
| 313 | + "engines": { | |
| 314 | + "node": ">=0.10.0" | |
| 315 | + } | |
| 316 | + }, | |
| 317 | + "node_modules/finalhandler": { | |
| 318 | + "version": "1.3.2", | |
| 319 | + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", | |
| 320 | + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", | |
| 321 | + "license": "MIT", | |
| 322 | + "dependencies": { | |
| 323 | + "debug": "2.6.9", | |
| 324 | + "encodeurl": "~2.0.0", | |
| 325 | + "escape-html": "~1.0.3", | |
| 326 | + "on-finished": "~2.4.1", | |
| 327 | + "parseurl": "~1.3.3", | |
| 328 | + "statuses": "~2.0.2", | |
| 329 | + "unpipe": "~1.0.0" | |
| 330 | + }, | |
| 331 | + "engines": { | |
| 332 | + "node": ">= 0.8" | |
| 333 | + } | |
| 334 | + }, | |
| 335 | + "node_modules/forwarded": { | |
| 336 | + "version": "0.2.0", | |
| 337 | + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", | |
| 338 | + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", | |
| 339 | + "license": "MIT", | |
| 340 | + "engines": { | |
| 341 | + "node": ">= 0.6" | |
| 342 | + } | |
| 343 | + }, | |
| 344 | + "node_modules/fresh": { | |
| 345 | + "version": "0.5.2", | |
| 346 | + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", | |
| 347 | + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", | |
| 348 | + "license": "MIT", | |
| 349 | + "engines": { | |
| 350 | + "node": ">= 0.6" | |
| 351 | + } | |
| 352 | + }, | |
| 353 | + "node_modules/function-bind": { | |
| 354 | + "version": "1.1.2", | |
| 355 | + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", | |
| 356 | + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", | |
| 357 | + "license": "MIT", | |
| 358 | + "funding": { | |
| 359 | + "url": "https://github.com/sponsors/ljharb" | |
| 360 | + } | |
| 361 | + }, | |
| 362 | + "node_modules/get-intrinsic": { | |
| 363 | + "version": "1.3.0", | |
| 364 | + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", | |
| 365 | + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", | |
| 366 | + "license": "MIT", | |
| 367 | + "dependencies": { | |
| 368 | + "call-bind-apply-helpers": "^1.0.2", | |
| 369 | + "es-define-property": "^1.0.1", | |
| 370 | + "es-errors": "^1.3.0", | |
| 371 | + "es-object-atoms": "^1.1.1", | |
| 372 | + "function-bind": "^1.1.2", | |
| 373 | + "get-proto": "^1.0.1", | |
| 374 | + "gopd": "^1.2.0", | |
| 375 | + "has-symbols": "^1.1.0", | |
| 376 | + "hasown": "^2.0.2", | |
| 377 | + "math-intrinsics": "^1.1.0" | |
| 378 | + }, | |
| 379 | + "engines": { | |
| 380 | + "node": ">= 0.4" | |
| 381 | + }, | |
| 382 | + "funding": { | |
| 383 | + "url": "https://github.com/sponsors/ljharb" | |
| 384 | + } | |
| 385 | + }, | |
| 386 | + "node_modules/get-proto": { | |
| 387 | + "version": "1.0.1", | |
| 388 | + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", | |
| 389 | + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", | |
| 390 | + "license": "MIT", | |
| 391 | + "dependencies": { | |
| 392 | + "dunder-proto": "^1.0.1", | |
| 393 | + "es-object-atoms": "^1.0.0" | |
| 394 | + }, | |
| 395 | + "engines": { | |
| 396 | + "node": ">= 0.4" | |
| 397 | + } | |
| 398 | + }, | |
| 399 | + "node_modules/gopd": { | |
| 400 | + "version": "1.2.0", | |
| 401 | + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", | |
| 402 | + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", | |
| 403 | + "license": "MIT", | |
| 404 | + "engines": { | |
| 405 | + "node": ">= 0.4" | |
| 406 | + }, | |
| 407 | + "funding": { | |
| 408 | + "url": "https://github.com/sponsors/ljharb" | |
| 409 | + } | |
| 410 | + }, | |
| 411 | + "node_modules/gray-matter": { | |
| 412 | + "version": "4.0.3", | |
| 413 | + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", | |
| 414 | + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", | |
| 415 | + "license": "MIT", | |
| 416 | + "dependencies": { | |
| 417 | + "js-yaml": "^3.13.1", | |
| 418 | + "kind-of": "^6.0.2", | |
| 419 | + "section-matter": "^1.0.0", | |
| 420 | + "strip-bom-string": "^1.0.0" | |
| 421 | + }, | |
| 422 | + "engines": { | |
| 423 | + "node": ">=6.0" | |
| 424 | + } | |
| 425 | + }, | |
| 426 | + "node_modules/has-symbols": { | |
| 427 | + "version": "1.1.0", | |
| 428 | + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", | |
| 429 | + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", | |
| 430 | + "license": "MIT", | |
| 431 | + "engines": { | |
| 432 | + "node": ">= 0.4" | |
| 433 | + }, | |
| 434 | + "funding": { | |
| 435 | + "url": "https://github.com/sponsors/ljharb" | |
| 436 | + } | |
| 437 | + }, | |
| 438 | + "node_modules/hasown": { | |
| 439 | + "version": "2.0.4", | |
| 440 | + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", | |
| 441 | + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", | |
| 442 | + "license": "MIT", | |
| 443 | + "dependencies": { | |
| 444 | + "function-bind": "^1.1.2" | |
| 445 | + }, | |
| 446 | + "engines": { | |
| 447 | + "node": ">= 0.4" | |
| 448 | + } | |
| 449 | + }, | |
| 450 | + "node_modules/highlight.js": { | |
| 451 | + "version": "11.12.0", | |
| 452 | + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.12.0.tgz", | |
| 453 | + "integrity": "sha512-nbfWpyRMcMrPMmDwJB+dhX/eiaPKtc2RB+0QZskqJ3WjRA/FDS0e9hZrx8EC/lbEv8gXy98FcDbNa/dspAaJMg==", | |
| 454 | + "license": "BSD-3-Clause", | |
| 455 | + "engines": { | |
| 456 | + "node": ">=12.0.0" | |
| 457 | + } | |
| 458 | + }, | |
| 459 | + "node_modules/http-errors": { | |
| 460 | + "version": "2.0.1", | |
| 461 | + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", | |
| 462 | + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", | |
| 463 | + "license": "MIT", | |
| 464 | + "dependencies": { | |
| 465 | + "depd": "~2.0.0", | |
| 466 | + "inherits": "~2.0.4", | |
| 467 | + "setprototypeof": "~1.2.0", | |
| 468 | + "statuses": "~2.0.2", | |
| 469 | + "toidentifier": "~1.0.1" | |
| 470 | + }, | |
| 471 | + "engines": { | |
| 472 | + "node": ">= 0.8" | |
| 473 | + }, | |
| 474 | + "funding": { | |
| 475 | + "type": "opencollective", | |
| 476 | + "url": "https://opencollective.com/express" | |
| 477 | + } | |
| 478 | + }, | |
| 479 | + "node_modules/iconv-lite": { | |
| 480 | + "version": "0.4.24", | |
| 481 | + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", | |
| 482 | + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", | |
| 483 | + "license": "MIT", | |
| 484 | + "dependencies": { | |
| 485 | + "safer-buffer": ">= 2.1.2 < 3" | |
| 486 | + }, | |
| 487 | + "engines": { | |
| 488 | + "node": ">=0.10.0" | |
| 489 | + } | |
| 490 | + }, | |
| 491 | + "node_modules/inherits": { | |
| 492 | + "version": "2.0.4", | |
| 493 | + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", | |
| 494 | + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", | |
| 495 | + "license": "ISC" | |
| 496 | + }, | |
| 497 | + "node_modules/ipaddr.js": { | |
| 498 | + "version": "1.9.1", | |
| 499 | + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", | |
| 500 | + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", | |
| 501 | + "license": "MIT", | |
| 502 | + "engines": { | |
| 503 | + "node": ">= 0.10" | |
| 504 | + } | |
| 505 | + }, | |
| 506 | + "node_modules/is-extendable": { | |
| 507 | + "version": "0.1.1", | |
| 508 | + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", | |
| 509 | + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", | |
| 510 | + "license": "MIT", | |
| 511 | + "engines": { | |
| 512 | + "node": ">=0.10.0" | |
| 513 | + } | |
| 514 | + }, | |
| 515 | + "node_modules/js-yaml": { | |
| 516 | + "version": "3.15.1", | |
| 517 | + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.1.tgz", | |
| 518 | + "integrity": "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==", | |
| 519 | + "license": "MIT", | |
| 520 | + "dependencies": { | |
| 521 | + "argparse": "^1.0.7", | |
| 522 | + "esprima": "^4.0.0" | |
| 523 | + }, | |
| 524 | + "bin": { | |
| 525 | + "js-yaml": "bin/js-yaml.js" | |
| 526 | + } | |
| 527 | + }, | |
| 528 | + "node_modules/kind-of": { | |
| 529 | + "version": "6.0.3", | |
| 530 | + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", | |
| 531 | + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", | |
| 532 | + "license": "MIT", | |
| 533 | + "engines": { | |
| 534 | + "node": ">=0.10.0" | |
| 535 | + } | |
| 536 | + }, | |
| 537 | + "node_modules/marked": { | |
| 538 | + "version": "14.1.4", | |
| 539 | + "resolved": "https://registry.npmjs.org/marked/-/marked-14.1.4.tgz", | |
| 540 | + "integrity": "sha512-vkVZ8ONmUdPnjCKc5uTRvmkRbx4EAi2OkTOXmfTDhZz3OFqMNBM1oTTWwTr4HY4uAEojhzPf+Fy8F1DWa3Sndg==", | |
| 541 | + "license": "MIT", | |
| 542 | + "bin": { | |
| 543 | + "marked": "bin/marked.js" | |
| 544 | + }, | |
| 545 | + "engines": { | |
| 546 | + "node": ">= 18" | |
| 547 | + } | |
| 548 | + }, | |
| 549 | + "node_modules/math-intrinsics": { | |
| 550 | + "version": "1.1.0", | |
| 551 | + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", | |
| 552 | + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", | |
| 553 | + "license": "MIT", | |
| 554 | + "engines": { | |
| 555 | + "node": ">= 0.4" | |
| 556 | + } | |
| 557 | + }, | |
| 558 | + "node_modules/media-typer": { | |
| 559 | + "version": "0.3.0", | |
| 560 | + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", | |
| 561 | + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", | |
| 562 | + "license": "MIT", | |
| 563 | + "engines": { | |
| 564 | + "node": ">= 0.6" | |
| 565 | + } | |
| 566 | + }, | |
| 567 | + "node_modules/merge-descriptors": { | |
| 568 | + "version": "1.0.3", | |
| 569 | + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", | |
| 570 | + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", | |
| 571 | + "license": "MIT", | |
| 572 | + "funding": { | |
| 573 | + "url": "https://github.com/sponsors/sindresorhus" | |
| 574 | + } | |
| 575 | + }, | |
| 576 | + "node_modules/methods": { | |
| 577 | + "version": "1.1.2", | |
| 578 | + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", | |
| 579 | + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", | |
| 580 | + "license": "MIT", | |
| 581 | + "engines": { | |
| 582 | + "node": ">= 0.6" | |
| 583 | + } | |
| 584 | + }, | |
| 585 | + "node_modules/mime": { | |
| 586 | + "version": "1.6.0", | |
| 587 | + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", | |
| 588 | + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", | |
| 589 | + "license": "MIT", | |
| 590 | + "bin": { | |
| 591 | + "mime": "cli.js" | |
| 592 | + }, | |
| 593 | + "engines": { | |
| 594 | + "node": ">=4" | |
| 595 | + } | |
| 596 | + }, | |
| 597 | + "node_modules/mime-db": { | |
| 598 | + "version": "1.52.0", | |
| 599 | + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", | |
| 600 | + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", | |
| 601 | + "license": "MIT", | |
| 602 | + "engines": { | |
| 603 | + "node": ">= 0.6" | |
| 604 | + } | |
| 605 | + }, | |
| 606 | + "node_modules/mime-types": { | |
| 607 | + "version": "2.1.35", | |
| 608 | + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", | |
| 609 | + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", | |
| 610 | + "license": "MIT", | |
| 611 | + "dependencies": { | |
| 612 | + "mime-db": "1.52.0" | |
| 613 | + }, | |
| 614 | + "engines": { | |
| 615 | + "node": ">= 0.6" | |
| 616 | + } | |
| 617 | + }, | |
| 618 | + "node_modules/ms": { | |
| 619 | + "version": "2.0.0", | |
| 620 | + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", | |
| 621 | + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", | |
| 622 | + "license": "MIT" | |
| 623 | + }, | |
| 624 | + "node_modules/negotiator": { | |
| 625 | + "version": "0.6.3", | |
| 626 | + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", | |
| 627 | + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", | |
| 628 | + "license": "MIT", | |
| 629 | + "engines": { | |
| 630 | + "node": ">= 0.6" | |
| 631 | + } | |
| 632 | + }, | |
| 633 | + "node_modules/object-inspect": { | |
| 634 | + "version": "1.13.4", | |
| 635 | + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", | |
| 636 | + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", | |
| 637 | + "license": "MIT", | |
| 638 | + "engines": { | |
| 639 | + "node": ">= 0.4" | |
| 640 | + }, | |
| 641 | + "funding": { | |
| 642 | + "url": "https://github.com/sponsors/ljharb" | |
| 643 | + } | |
| 644 | + }, | |
| 645 | + "node_modules/on-finished": { | |
| 646 | + "version": "2.4.1", | |
| 647 | + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", | |
| 648 | + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", | |
| 649 | + "license": "MIT", | |
| 650 | + "dependencies": { | |
| 651 | + "ee-first": "1.1.1" | |
| 652 | + }, | |
| 653 | + "engines": { | |
| 654 | + "node": ">= 0.8" | |
| 655 | + } | |
| 656 | + }, | |
| 657 | + "node_modules/parseurl": { | |
| 658 | + "version": "1.3.3", | |
| 659 | + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", | |
| 660 | + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", | |
| 661 | + "license": "MIT", | |
| 662 | + "engines": { | |
| 663 | + "node": ">= 0.8" | |
| 664 | + } | |
| 665 | + }, | |
| 666 | + "node_modules/path-to-regexp": { | |
| 667 | + "version": "0.1.13", | |
| 668 | + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", | |
| 669 | + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", | |
| 670 | + "license": "MIT" | |
| 671 | + }, | |
| 672 | + "node_modules/proxy-addr": { | |
| 673 | + "version": "2.0.7", | |
| 674 | + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", | |
| 675 | + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", | |
| 676 | + "license": "MIT", | |
| 677 | + "dependencies": { | |
| 678 | + "forwarded": "0.2.0", | |
| 679 | + "ipaddr.js": "1.9.1" | |
| 680 | + }, | |
| 681 | + "engines": { | |
| 682 | + "node": ">= 0.10" | |
| 683 | + } | |
| 684 | + }, | |
| 685 | + "node_modules/qs": { | |
| 686 | + "version": "6.15.3", | |
| 687 | + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", | |
| 688 | + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", | |
| 689 | + "license": "BSD-3-Clause", | |
| 690 | + "dependencies": { | |
| 691 | + "es-define-property": "^1.0.1", | |
| 692 | + "side-channel": "^1.1.1" | |
| 693 | + }, | |
| 694 | + "engines": { | |
| 695 | + "node": ">=0.6" | |
| 696 | + }, | |
| 697 | + "funding": { | |
| 698 | + "url": "https://github.com/sponsors/ljharb" | |
| 699 | + } | |
| 700 | + }, | |
| 701 | + "node_modules/range-parser": { | |
| 702 | + "version": "1.2.1", | |
| 703 | + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", | |
| 704 | + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", | |
| 705 | + "license": "MIT", | |
| 706 | + "engines": { | |
| 707 | + "node": ">= 0.6" | |
| 708 | + } | |
| 709 | + }, | |
| 710 | + "node_modules/raw-body": { | |
| 711 | + "version": "2.5.3", | |
| 712 | + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", | |
| 713 | + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", | |
| 714 | + "license": "MIT", | |
| 715 | + "dependencies": { | |
| 716 | + "bytes": "~3.1.2", | |
| 717 | + "http-errors": "~2.0.1", | |
| 718 | + "iconv-lite": "~0.4.24", | |
| 719 | + "unpipe": "~1.0.0" | |
| 720 | + }, | |
| 721 | + "engines": { | |
| 722 | + "node": ">= 0.8" | |
| 723 | + } | |
| 724 | + }, | |
| 725 | + "node_modules/safe-buffer": { | |
| 726 | + "version": "5.2.1", | |
| 727 | + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", | |
| 728 | + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", | |
| 729 | + "funding": [ | |
| 730 | + { | |
| 731 | + "type": "github", | |
| 732 | + "url": "https://github.com/sponsors/feross" | |
| 733 | + }, | |
| 734 | + { | |
| 735 | + "type": "patreon", | |
| 736 | + "url": "https://www.patreon.com/feross" | |
| 737 | + }, | |
| 738 | + { | |
| 739 | + "type": "consulting", | |
| 740 | + "url": "https://feross.org/support" | |
| 741 | + } | |
| 742 | + ], | |
| 743 | + "license": "MIT" | |
| 744 | + }, | |
| 745 | + "node_modules/safer-buffer": { | |
| 746 | + "version": "2.1.2", | |
| 747 | + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", | |
| 748 | + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", | |
| 749 | + "license": "MIT" | |
| 750 | + }, | |
| 751 | + "node_modules/section-matter": { | |
| 752 | + "version": "1.0.0", | |
| 753 | + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", | |
| 754 | + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", | |
| 755 | + "license": "MIT", | |
| 756 | + "dependencies": { | |
| 757 | + "extend-shallow": "^2.0.1", | |
| 758 | + "kind-of": "^6.0.0" | |
| 759 | + }, | |
| 760 | + "engines": { | |
| 761 | + "node": ">=4" | |
| 762 | + } | |
| 763 | + }, | |
| 764 | + "node_modules/send": { | |
| 765 | + "version": "0.19.2", | |
| 766 | + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", | |
| 767 | + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", | |
| 768 | + "license": "MIT", | |
| 769 | + "dependencies": { | |
| 770 | + "debug": "2.6.9", | |
| 771 | + "depd": "2.0.0", | |
| 772 | + "destroy": "1.2.0", | |
| 773 | + "encodeurl": "~2.0.0", | |
| 774 | + "escape-html": "~1.0.3", | |
| 775 | + "etag": "~1.8.1", | |
| 776 | + "fresh": "~0.5.2", | |
| 777 | + "http-errors": "~2.0.1", | |
| 778 | + "mime": "1.6.0", | |
| 779 | + "ms": "2.1.3", | |
| 780 | + "on-finished": "~2.4.1", | |
| 781 | + "range-parser": "~1.2.1", | |
| 782 | + "statuses": "~2.0.2" | |
| 783 | + }, | |
| 784 | + "engines": { | |
| 785 | + "node": ">= 0.8.0" | |
| 786 | + } | |
| 787 | + }, | |
| 788 | + "node_modules/send/node_modules/ms": { | |
| 789 | + "version": "2.1.3", | |
| 790 | + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", | |
| 791 | + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", | |
| 792 | + "license": "MIT" | |
| 793 | + }, | |
| 794 | + "node_modules/serve-static": { | |
| 795 | + "version": "1.16.3", | |
| 796 | + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", | |
| 797 | + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", | |
| 798 | + "license": "MIT", | |
| 799 | + "dependencies": { | |
| 800 | + "encodeurl": "~2.0.0", | |
| 801 | + "escape-html": "~1.0.3", | |
| 802 | + "parseurl": "~1.3.3", | |
| 803 | + "send": "~0.19.1" | |
| 804 | + }, | |
| 805 | + "engines": { | |
| 806 | + "node": ">= 0.8.0" | |
| 807 | + } | |
| 808 | + }, | |
| 809 | + "node_modules/setprototypeof": { | |
| 810 | + "version": "1.2.0", | |
| 811 | + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", | |
| 812 | + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", | |
| 813 | + "license": "ISC" | |
| 814 | + }, | |
| 815 | + "node_modules/side-channel": { | |
| 816 | + "version": "1.1.1", | |
| 817 | + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", | |
| 818 | + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", | |
| 819 | + "license": "MIT", | |
| 820 | + "dependencies": { | |
| 821 | + "es-errors": "^1.3.0", | |
| 822 | + "object-inspect": "^1.13.4", | |
| 823 | + "side-channel-list": "^1.0.1", | |
| 824 | + "side-channel-map": "^1.0.1", | |
| 825 | + "side-channel-weakmap": "^1.0.2" | |
| 826 | + }, | |
| 827 | + "engines": { | |
| 828 | + "node": ">= 0.4" | |
| 829 | + }, | |
| 830 | + "funding": { | |
| 831 | + "url": "https://github.com/sponsors/ljharb" | |
| 832 | + } | |
| 833 | + }, | |
| 834 | + "node_modules/side-channel-list": { | |
| 835 | + "version": "1.0.1", | |
| 836 | + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", | |
| 837 | + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", | |
| 838 | + "license": "MIT", | |
| 839 | + "dependencies": { | |
| 840 | + "es-errors": "^1.3.0", | |
| 841 | + "object-inspect": "^1.13.4" | |
| 842 | + }, | |
| 843 | + "engines": { | |
| 844 | + "node": ">= 0.4" | |
| 845 | + }, | |
| 846 | + "funding": { | |
| 847 | + "url": "https://github.com/sponsors/ljharb" | |
| 848 | + } | |
| 849 | + }, | |
| 850 | + "node_modules/side-channel-map": { | |
| 851 | + "version": "1.0.1", | |
| 852 | + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", | |
| 853 | + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", | |
| 854 | + "license": "MIT", | |
| 855 | + "dependencies": { | |
| 856 | + "call-bound": "^1.0.2", | |
| 857 | + "es-errors": "^1.3.0", | |
| 858 | + "get-intrinsic": "^1.2.5", | |
| 859 | + "object-inspect": "^1.13.3" | |
| 860 | + }, | |
| 861 | + "engines": { | |
| 862 | + "node": ">= 0.4" | |
| 863 | + }, | |
| 864 | + "funding": { | |
| 865 | + "url": "https://github.com/sponsors/ljharb" | |
| 866 | + } | |
| 867 | + }, | |
| 868 | + "node_modules/side-channel-weakmap": { | |
| 869 | + "version": "1.0.2", | |
| 870 | + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", | |
| 871 | + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", | |
| 872 | + "license": "MIT", | |
| 873 | + "dependencies": { | |
| 874 | + "call-bound": "^1.0.2", | |
| 875 | + "es-errors": "^1.3.0", | |
| 876 | + "get-intrinsic": "^1.2.5", | |
| 877 | + "object-inspect": "^1.13.3", | |
| 878 | + "side-channel-map": "^1.0.1" | |
| 879 | + }, | |
| 880 | + "engines": { | |
| 881 | + "node": ">= 0.4" | |
| 882 | + }, | |
| 883 | + "funding": { | |
| 884 | + "url": "https://github.com/sponsors/ljharb" | |
| 885 | + } | |
| 886 | + }, | |
| 887 | + "node_modules/sprintf-js": { | |
| 888 | + "version": "1.0.3", | |
| 889 | + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", | |
| 890 | + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", | |
| 891 | + "license": "BSD-3-Clause" | |
| 892 | + }, | |
| 893 | + "node_modules/statuses": { | |
| 894 | + "version": "2.0.2", | |
| 895 | + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", | |
| 896 | + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", | |
| 897 | + "license": "MIT", | |
| 898 | + "engines": { | |
| 899 | + "node": ">= 0.8" | |
| 900 | + } | |
| 901 | + }, | |
| 902 | + "node_modules/strip-bom-string": { | |
| 903 | + "version": "1.0.0", | |
| 904 | + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", | |
| 905 | + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", | |
| 906 | + "license": "MIT", | |
| 907 | + "engines": { | |
| 908 | + "node": ">=0.10.0" | |
| 909 | + } | |
| 910 | + }, | |
| 911 | + "node_modules/toidentifier": { | |
| 912 | + "version": "1.0.1", | |
| 913 | + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", | |
| 914 | + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", | |
| 915 | + "license": "MIT", | |
| 916 | + "engines": { | |
| 917 | + "node": ">=0.6" | |
| 918 | + } | |
| 919 | + }, | |
| 920 | + "node_modules/type-is": { | |
| 921 | + "version": "1.6.18", | |
| 922 | + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", | |
| 923 | + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", | |
| 924 | + "license": "MIT", | |
| 925 | + "dependencies": { | |
| 926 | + "media-typer": "0.3.0", | |
| 927 | + "mime-types": "~2.1.24" | |
| 928 | + }, | |
| 929 | + "engines": { | |
| 930 | + "node": ">= 0.6" | |
| 931 | + } | |
| 932 | + }, | |
| 933 | + "node_modules/unpipe": { | |
| 934 | + "version": "1.0.0", | |
| 935 | + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", | |
| 936 | + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", | |
| 937 | + "license": "MIT", | |
| 938 | + "engines": { | |
| 939 | + "node": ">= 0.8" | |
| 940 | + } | |
| 941 | + }, | |
| 942 | + "node_modules/utils-merge": { | |
| 943 | + "version": "1.0.1", | |
| 944 | + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", | |
| 945 | + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", | |
| 946 | + "license": "MIT", | |
| 947 | + "engines": { | |
| 948 | + "node": ">= 0.4.0" | |
| 949 | + } | |
| 950 | + }, | |
| 951 | + "node_modules/vary": { | |
| 952 | + "version": "1.1.2", | |
| 953 | + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", | |
| 954 | + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", | |
| 955 | + "license": "MIT", | |
| 956 | + "engines": { | |
| 957 | + "node": ">= 0.8" | |
| 958 | + } | |
| 959 | + } | |
| 960 | + } | |
| 961 | +} | |
added
web/package.json
+18 −0
@@ -0,0 +1,18 @@ | ||
| 1 | +{ | |
| 2 | + "name": "localvm-web", | |
| 3 | + "version": "1.0.0", | |
| 4 | + "description": "Research showcase platform for localvm-research — Simon-Pierre Boucher <contact@spboucher.ai>", | |
| 5 | + "author": "Simon-Pierre Boucher <contact@spboucher.ai>", | |
| 6 | + "license": "UNLICENSED", | |
| 7 | + "private": true, | |
| 8 | + "main": "server.js", | |
| 9 | + "scripts": { | |
| 10 | + "start": "node server.js" | |
| 11 | + }, | |
| 12 | + "dependencies": { | |
| 13 | + "express": "^4.21.0", | |
| 14 | + "gray-matter": "^4.0.3", | |
| 15 | + "highlight.js": "^11.10.0", | |
| 16 | + "marked": "^14.1.0" | |
| 17 | + } | |
| 18 | +} | |
added
web/public/app.js
+31 −0
@@ -0,0 +1,31 @@ | ||
| 1 | +// ============================================================================ | |
| 2 | +// Project : localvm-research | |
| 3 | +// File : web/public/app.js | |
| 4 | +// Purpose : Client-side interactivity (chart hover tooltips) | |
| 5 | +// Author : Simon-Pierre Boucher | |
| 6 | +// Contact : contact@spboucher.ai | |
| 7 | +// Created : 2026-08-12 | |
| 8 | +// Modified : 2026-08-12 | |
| 9 | +// Platform : Web (deployed from macOS / Apple Silicon) | |
| 10 | +// License : All rights reserved (research code) | |
| 11 | +// ============================================================================ | |
| 12 | +"use strict"; | |
| 13 | + | |
| 14 | +(function () { | |
| 15 | + const fig = document.querySelector(".chart-fig"); | |
| 16 | + if (!fig) return; | |
| 17 | + const tip = fig.querySelector(".chart-tip"); | |
| 18 | + if (!tip) return; | |
| 19 | + fig.querySelectorAll(".pt").forEach((pt) => { | |
| 20 | + pt.addEventListener("mouseenter", () => { | |
| 21 | + tip.textContent = pt.getAttribute("data-tip"); | |
| 22 | + tip.hidden = false; | |
| 23 | + }); | |
| 24 | + pt.addEventListener("mousemove", (ev) => { | |
| 25 | + const r = fig.getBoundingClientRect(); | |
| 26 | + tip.style.left = Math.min(ev.clientX - r.left + 14, r.width - tip.offsetWidth - 4) + "px"; | |
| 27 | + tip.style.top = ev.clientY - r.top - 34 + "px"; | |
| 28 | + }); | |
| 29 | + pt.addEventListener("mouseleave", () => { tip.hidden = true; }); | |
| 30 | + }); | |
| 31 | +})(); | |
added
web/public/style.css
+208 −0
@@ -0,0 +1,208 @@ | ||
| 1 | +/* ============================================================================ | |
| 2 | + * Project : localvm-research | |
| 3 | + * File : web/public/style.css | |
| 4 | + * Purpose : Light-theme styling for the research showcase platform | |
| 5 | + * Author : Simon-Pierre Boucher | |
| 6 | + * Contact : contact@spboucher.ai | |
| 7 | + * Created : 2026-08-12 | |
| 8 | + * Modified : 2026-08-12 | |
| 9 | + * Platform : Web (deployed from macOS / Apple Silicon) | |
| 10 | + * License : All rights reserved (research code) | |
| 11 | + * ==========================================================================*/ | |
| 12 | + | |
| 13 | +:root { | |
| 14 | + color-scheme: light; | |
| 15 | + --page: #f9f9f7; | |
| 16 | + --surface: #fcfcfb; | |
| 17 | + --ink: #0b0b0b; | |
| 18 | + --ink-2: #52514e; | |
| 19 | + --muted: #898781; | |
| 20 | + --grid: #e1e0d9; | |
| 21 | + --baseline: #c3c2b7; | |
| 22 | + --border: rgba(11, 11, 11, 0.10); | |
| 23 | + --accent: #2a78d6; | |
| 24 | + --accent-dark: #1c5cab; | |
| 25 | + --good: #0ca30c; | |
| 26 | + --good-text: #006300; | |
| 27 | + --warn: #eda100; | |
| 28 | +} | |
| 29 | + | |
| 30 | +* { box-sizing: border-box; } | |
| 31 | +html { -webkit-text-size-adjust: 100%; } | |
| 32 | +body { | |
| 33 | + margin: 0; | |
| 34 | + font-family: system-ui, -apple-system, "Segoe UI", sans-serif; | |
| 35 | + background: var(--page); | |
| 36 | + color: var(--ink); | |
| 37 | + line-height: 1.6; | |
| 38 | + font-size: 16px; | |
| 39 | +} | |
| 40 | +.wrap { max-width: 1080px; margin: 0 auto; padding: 0 24px; } | |
| 41 | +a { color: var(--accent-dark); text-decoration: none; } | |
| 42 | +a:hover { text-decoration: underline; } | |
| 43 | +code, pre, .mono-small { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; } | |
| 44 | + | |
| 45 | +/* header */ | |
| 46 | +.site-header { | |
| 47 | + background: var(--surface); | |
| 48 | + border-bottom: 1px solid var(--grid); | |
| 49 | + position: sticky; top: 0; z-index: 10; | |
| 50 | +} | |
| 51 | +.header-row { display: flex; align-items: center; justify-content: space-between; height: 56px; } | |
| 52 | +.brand { font-weight: 700; font-size: 18px; color: var(--ink); letter-spacing: -0.02em; } | |
| 53 | +.brand-dim { color: var(--muted); font-weight: 500; } | |
| 54 | +.site-nav { display: flex; gap: 4px; flex-wrap: wrap; } | |
| 55 | +.site-nav a { | |
| 56 | + padding: 6px 12px; border-radius: 6px; color: var(--ink-2); font-size: 14px; font-weight: 500; | |
| 57 | +} | |
| 58 | +.site-nav a:hover { background: var(--page); text-decoration: none; color: var(--ink); } | |
| 59 | +.site-nav a.active { color: var(--accent-dark); background: #edf3fb; } | |
| 60 | + | |
| 61 | +/* hero */ | |
| 62 | +.hero { padding: 56px 0 8px; } | |
| 63 | +.kicker { | |
| 64 | + text-transform: uppercase; letter-spacing: 0.08em; font-size: 12px; font-weight: 600; | |
| 65 | + color: var(--accent-dark); margin: 0 0 12px; | |
| 66 | +} | |
| 67 | +.hero h1 { font-size: 40px; line-height: 1.15; letter-spacing: -0.02em; margin: 0 0 16px; } | |
| 68 | +.lede { font-size: 18px; color: var(--ink-2); max-width: 760px; margin: 0 0 12px; } | |
| 69 | +.lede-small { color: var(--ink-2); max-width: 760px; } | |
| 70 | +.lede-eq code { | |
| 71 | + font-size: 13px; background: var(--surface); border: 1px solid var(--grid); | |
| 72 | + border-radius: 6px; padding: 6px 10px; color: var(--ink-2); display: inline-block; | |
| 73 | +} | |
| 74 | + | |
| 75 | +/* stat tiles */ | |
| 76 | +.tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 16px; margin: 32px 0; } | |
| 77 | +.tile { | |
| 78 | + background: var(--surface); border: 1px solid var(--grid); border-radius: 10px; | |
| 79 | + padding: 20px 22px; display: flex; flex-direction: column; gap: 2px; | |
| 80 | +} | |
| 81 | +.tile-value { font-size: 34px; font-weight: 700; letter-spacing: -0.02em; } | |
| 82 | +.tile-denom { font-size: 20px; color: var(--muted); font-weight: 500; } | |
| 83 | +.tile-label { color: var(--ink-2); font-size: 13.5px; } | |
| 84 | + | |
| 85 | +/* layout blocks */ | |
| 86 | +.split { display: grid; grid-template-columns: 3fr 2fr; gap: 20px; margin: 8px 0 20px; } | |
| 87 | +@media (max-width: 880px) { .split { grid-template-columns: 1fr; } .hero h1 { font-size: 30px; } } | |
| 88 | +.card { | |
| 89 | + background: var(--surface); border: 1px solid var(--grid); border-radius: 10px; | |
| 90 | + padding: 24px 26px; margin-bottom: 20px; | |
| 91 | +} | |
| 92 | +.card h2 { margin-top: 0; font-size: 20px; letter-spacing: -0.01em; } | |
| 93 | +.more { font-weight: 600; font-size: 14px; } | |
| 94 | + | |
| 95 | +/* phases */ | |
| 96 | +.phases { list-style: none; margin: 0; padding: 0; } | |
| 97 | +.phase { display: flex; align-items: flex-start; gap: 12px; padding: 10px 0; border-bottom: 1px solid var(--grid); } | |
| 98 | +.phase:last-child { border-bottom: 0; } | |
| 99 | +.phase strong { display: block; font-size: 14.5px; } | |
| 100 | +.phase-detail { font-size: 13px; color: var(--muted); } | |
| 101 | +.phase-dot { width: 10px; height: 10px; border-radius: 50%; margin-top: 7px; flex: 0 0 auto; background: var(--baseline); } | |
| 102 | +.phase.done .phase-dot { background: var(--good); } | |
| 103 | +.phase.in-progress .phase-dot { background: var(--accent); } | |
| 104 | +.phase .badge { margin-left: auto; } | |
| 105 | +.badge { | |
| 106 | + font-size: 11.5px; font-weight: 600; padding: 3px 9px; border-radius: 20px; | |
| 107 | + white-space: nowrap; align-self: center; border: 1px solid var(--grid); color: var(--ink-2); | |
| 108 | +} | |
| 109 | +.badge-done { color: var(--good-text); background: #eef7ee; border-color: #cfe8cf; } | |
| 110 | +.badge-in-progress, .badge-has-results { color: var(--accent-dark); background: #edf3fb; border-color: #cfe0f5; } | |
| 111 | +.badge-pending, .badge-scaffolded { color: var(--muted); background: var(--page); } | |
| 112 | + | |
| 113 | +/* doc cards */ | |
| 114 | +.page-title { font-size: 30px; letter-spacing: -0.02em; margin: 40px 0 8px; } | |
| 115 | +.section-title { font-size: 20px; margin: 32px 0 8px; } | |
| 116 | +.doc-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; margin: 16px 0; } | |
| 117 | +.doc-card { | |
| 118 | + background: var(--surface); border: 1px solid var(--grid); border-radius: 10px; | |
| 119 | + padding: 18px 20px; color: var(--ink); display: block; | |
| 120 | +} | |
| 121 | +.doc-card:hover { border-color: var(--accent); text-decoration: none; } | |
| 122 | +.doc-card.disabled { opacity: 0.55; pointer-events: none; } | |
| 123 | +.doc-card h3 { margin: 0 0 6px; font-size: 16px; } | |
| 124 | +.doc-card p { margin: 0 0 8px; font-size: 13.5px; color: var(--ink-2); } | |
| 125 | +.runs { font-size: 12px; color: var(--muted); margin-left: 8px; } | |
| 126 | +.note-line { color: var(--ink-2); font-size: 14px; } | |
| 127 | + | |
| 128 | +/* markdown body */ | |
| 129 | +.doc { max-width: 860px; margin: 32px auto; } | |
| 130 | +.crumb { font-size: 13px; color: var(--muted); margin: 24px 0 4px; } | |
| 131 | +.chips { display: flex; flex-wrap: wrap; gap: 8px; margin: 10px 0 18px; } | |
| 132 | +.chip { | |
| 133 | + font-size: 12px; background: var(--surface); border: 1px solid var(--grid); | |
| 134 | + border-radius: 20px; padding: 3px 12px; color: var(--ink-2); | |
| 135 | +} | |
| 136 | +.chip-k { color: var(--muted); margin-right: 6px; text-transform: uppercase; font-size: 10px; letter-spacing: 0.05em; } | |
| 137 | +.md { overflow-wrap: break-word; } | |
| 138 | +.md h1 { font-size: 28px; letter-spacing: -0.02em; } | |
| 139 | +.md h2 { font-size: 21px; margin-top: 36px; border-bottom: 1px solid var(--grid); padding-bottom: 6px; } | |
| 140 | +.md h3 { font-size: 17px; margin-top: 28px; } | |
| 141 | +.md code { background: #f1f0ec; border-radius: 4px; padding: 1px 5px; font-size: 0.88em; } | |
| 142 | +.md pre code, .codeblock code { background: none; padding: 0; font-size: 13px; } | |
| 143 | +.codeblock, .md pre { | |
| 144 | + background: #f6f5f2; border: 1px solid var(--grid); border-radius: 8px; | |
| 145 | + padding: 14px 16px; overflow-x: auto; line-height: 1.5; | |
| 146 | +} | |
| 147 | +.md table { border-collapse: collapse; width: 100%; font-size: 14px; display: block; overflow-x: auto; } | |
| 148 | +.md th, .md td { border: 1px solid var(--grid); padding: 6px 10px; text-align: left; vertical-align: top; } | |
| 149 | +.md th { background: var(--page); } | |
| 150 | +.md blockquote { border-left: 3px solid var(--accent); margin-left: 0; padding-left: 16px; color: var(--ink-2); } | |
| 151 | + | |
| 152 | +/* log */ | |
| 153 | +.log-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 20px; } | |
| 154 | +.log-entry h3 { font-size: 15px; margin: 0 0 8px; } | |
| 155 | +.log-entry .md { font-size: 13.5px; color: var(--ink-2); } | |
| 156 | + | |
| 157 | +/* results table / kv table / file lists */ | |
| 158 | +.results-table, .kv-table { border-collapse: collapse; width: 100%; font-size: 14.5px; background: var(--surface); } | |
| 159 | +.results-table th, .results-table td, .kv-table th, .kv-table td { | |
| 160 | + border: 1px solid var(--grid); padding: 8px 12px; text-align: left; | |
| 161 | +} | |
| 162 | +.results-table th { background: var(--page); } | |
| 163 | +.kv-table th { background: var(--page); width: 180px; } | |
| 164 | +.file-list { list-style: none; padding: 0; } | |
| 165 | +.file-list li { padding: 6px 0; border-bottom: 1px solid var(--grid); } | |
| 166 | +.tree { list-style: none; padding-left: 0; } | |
| 167 | +.tree ul { list-style: none; padding-left: 20px; } | |
| 168 | +.tree-root { margin-bottom: 14px; } | |
| 169 | +.tree li { padding: 2px 0; font-size: 14.5px; } | |
| 170 | +.mono-small { font-size: 12.5px; color: var(--muted); } | |
| 171 | +.file-card { padding: 0; overflow: hidden; } | |
| 172 | +.file-head { display: flex; justify-content: space-between; padding: 10px 16px; border-bottom: 1px solid var(--grid); background: var(--page); } | |
| 173 | +.file-card .codeblock { border: 0; border-radius: 0; margin: 0; } | |
| 174 | + | |
| 175 | +/* chart */ | |
| 176 | +.chart-fig { margin: 18px 0 8px; position: relative; } | |
| 177 | +.chart-fig svg { width: 100%; height: auto; } | |
| 178 | +.chart-fig .grid { stroke: var(--grid); stroke-width: 1; } | |
| 179 | +.chart-fig .axis { stroke: var(--baseline); stroke-width: 1; } | |
| 180 | +.chart-fig .tick { fill: var(--muted); font-size: 11px; font-family: ui-monospace, Menlo, monospace; } | |
| 181 | +.chart-fig .axis-title { fill: var(--ink-2); font-size: 12px; } | |
| 182 | +.chart-fig .series-label { font-size: 12px; font-weight: 600; } | |
| 183 | +.chart-fig .pt { cursor: pointer; } | |
| 184 | +.chart-fig figcaption { font-size: 12.5px; color: var(--muted); margin-top: 4px; } | |
| 185 | +.chart-tip { | |
| 186 | + position: absolute; pointer-events: none; background: var(--ink); color: #fff; | |
| 187 | + font-size: 12.5px; padding: 5px 10px; border-radius: 6px; white-space: nowrap; z-index: 5; | |
| 188 | +} | |
| 189 | + | |
| 190 | +/* hljs light theme (subset, GitHub-like) */ | |
| 191 | +.hljs { color: #24292e; } | |
| 192 | +.hljs-keyword, .hljs-meta .hljs-keyword { color: #d73a49; } | |
| 193 | +.hljs-string, .hljs-attr { color: #032f62; } | |
| 194 | +.hljs-comment, .hljs-quote { color: #6a737d; font-style: italic; } | |
| 195 | +.hljs-number, .hljs-literal { color: #005cc5; } | |
| 196 | +.hljs-title, .hljs-function .hljs-title { color: #6f42c1; } | |
| 197 | +.hljs-built_in, .hljs-type { color: #e36209; } | |
| 198 | +.hljs-section { color: #005cc5; font-weight: 600; } | |
| 199 | +.hljs-params { color: #24292e; } | |
| 200 | +.hljs-variable, .hljs-template-variable { color: #e36209; } | |
| 201 | + | |
| 202 | +/* footer */ | |
| 203 | +.site-footer { | |
| 204 | + border-top: 1px solid var(--grid); margin-top: 48px; padding: 20px 0 32px; | |
| 205 | + background: var(--surface); font-size: 13px; color: var(--ink-2); | |
| 206 | +} | |
| 207 | +.site-footer .wrap { display: flex; justify-content: space-between; flex-wrap: wrap; gap: 8px; } | |
| 208 | +.footer-meta { color: var(--muted); } | |
added
web/server.js
+315 −0
@@ -0,0 +1,315 @@ | ||
| 1 | +// ============================================================================ | |
| 2 | +// Project : localvm-research | |
| 3 | +// File : web/server.js | |
| 4 | +// Purpose : Research showcase platform — routes and pages (English, light) | |
| 5 | +// Author : Simon-Pierre Boucher | |
| 6 | +// Contact : contact@spboucher.ai | |
| 7 | +// Created : 2026-08-12 | |
| 8 | +// Modified : 2026-08-12 | |
| 9 | +// Platform : macOS / Apple Silicon (arm64) — Node.js (deployed on MacLustr) | |
| 10 | +// License : All rights reserved (research code) | |
| 11 | +// ============================================================================ | |
| 12 | +"use strict"; | |
| 13 | + | |
| 14 | +const path = require("path"); | |
| 15 | +const express = require("express"); | |
| 16 | +const C = require("./lib/content"); | |
| 17 | +const R = require("./lib/render"); | |
| 18 | + | |
| 19 | +const app = express(); | |
| 20 | +const PORT = process.env.PORT || 8120; | |
| 21 | + | |
| 22 | +app.use("/static", express.static(path.join(__dirname, "public"), { maxAge: "1h" })); | |
| 23 | + | |
| 24 | +function page(res, opts) { | |
| 25 | + res.send(R.layout({ ...opts, buildInfo: C.buildInfo() })); | |
| 26 | +} | |
| 27 | + | |
| 28 | +function metaChips(data) { | |
| 29 | + if (!data || !Object.keys(data).length) return ""; | |
| 30 | + const fields = ["document", "author", "created", "modified", "status"]; | |
| 31 | + const chips = fields | |
| 32 | + .filter((f) => data[f]) | |
| 33 | + .map((f) => `<span class="chip"><span class="chip-k">${f}</span>${R.esc(data[f])}</span>`) | |
| 34 | + .join(""); | |
| 35 | + return chips ? `<div class="chips">${chips}</div>` : ""; | |
| 36 | +} | |
| 37 | + | |
| 38 | +// ---------------------------------------------------------------- home | |
| 39 | +app.get("/", (req, res) => { | |
| 40 | + const s = C.stats(); | |
| 41 | + const chart = C.expHChartData(); | |
| 42 | + const log = C.parseLogEntries().slice(-3).reverse(); | |
| 43 | + const phases = [ | |
| 44 | + ["Phase 1 — Literature sweep (§4.1–4.10)", "done", `${s.notes} theme notes · ${s.sources} sources`], | |
| 45 | + ["Phase 2 — State-of-the-art map", C.exists("research/state_of_the_art.md") ? "done" : "pending", "technique taxonomy + overlap analysis"], | |
| 46 | + ["Phase 3 — Research gaps", s.gaps ? "done" : "pending", `${s.gaps} falsifiable approaches (G01–G${String(s.gaps).padStart(2, "0")})`], | |
| 47 | + ["Phase 4 — Candidate ranking", C.exists("research/candidate_ranking.md") ? "done" : "in progress", "10-axis scoring, 3–5 candidates"], | |
| 48 | + ["Phases 5–6 — Framework & micro-experiments", s.experimentsDone ? "in progress" : "pending", `${s.experimentsDone}/${s.experiments} experiments completed`], | |
| 49 | + ["Phases 7–11 — Prototypes → novelty check", "pending", "evidence-driven"], | |
| 50 | + ]; | |
| 51 | + const phaseHtml = phases | |
| 52 | + .map( | |
| 53 | + ([name, st, detail]) => `<li class="phase ${st.replace(" ", "-")}"> | |
| 54 | + <span class="phase-dot"></span><div><strong>${name}</strong><span class="phase-detail">${detail}</span></div> | |
| 55 | + <span class="badge badge-${st.replace(" ", "-")}">${st}</span></li>` | |
| 56 | + ) | |
| 57 | + .join(""); | |
| 58 | + const logHtml = log | |
| 59 | + .map( | |
| 60 | + (e) => `<article class="log-entry"><h3>${R.esc(e.title)}</h3> | |
| 61 | + <div class="md">${R.markdownToHtml(e.body.split("\n").slice(0, 8).join("\n"), "research/LOG.md")}</div> | |
| 62 | + <a class="more" href="/doc/research/LOG.md">Full research log →</a></article>` | |
| 63 | + ) | |
| 64 | + .join(""); | |
| 65 | + | |
| 66 | + const body = ` | |
| 67 | +<section class="hero"> | |
| 68 | + <p class="kicker">Independent systems + ML research · Apple Silicon</p> | |
| 69 | + <h1>Running LLMs larger than memory<br>on a consumer Mac</h1> | |
| 70 | + <p class="lede">Can an existing pretrained model be transformed <em>post-training</em> into an execution | |
| 71 | + representation whose instantaneous working set is dramatically smaller than the full checkpoint — | |
| 72 | + while preserving most of its capabilities? This platform exposes the complete paper trail: | |
| 73 | + hypotheses, notes, code, benchmarks, and raw results.</p> | |
| 74 | + <p class="lede-eq"><code>total model size ≠ resident size ≠ bytes read per token ≠ parameters required for this token</code></p> | |
| 75 | +</section> | |
| 76 | + | |
| 77 | +<section class="tiles"> | |
| 78 | + <div class="tile"><span class="tile-value">${s.sources}</span><span class="tile-label">sources reviewed</span></div> | |
| 79 | + <div class="tile"><span class="tile-value">${s.gaps}</span><span class="tile-label">research gaps identified</span></div> | |
| 80 | + <div class="tile"><span class="tile-value">${s.experimentsDone}<span class="tile-denom">/${s.experiments}</span></span><span class="tile-label">experiments completed</span></div> | |
| 81 | + <div class="tile"><span class="tile-value">${s.build.commits || "—"}</span><span class="tile-label">commits</span></div> | |
| 82 | +</section> | |
| 83 | + | |
| 84 | +<section class="split"> | |
| 85 | + <div class="card"> | |
| 86 | + <h2>First result — the SSD substrate is not the bottleneck</h2> | |
| 87 | + <p>Experiment H measured this Mac's internal NVMe under genuinely cold-cache conditions. | |
| 88 | + Random reads reach the ~13.1 GB/s device ceiling at 1 MiB blocks (QD 8), while | |
| 89 | + 4 KiB single-threaded reads manage only 67 MB/s — a 200× spread that dictates the | |
| 90 | + weight-block layout contract: <strong>≥ 256 KiB blocks at QD ≥ 4</strong>. Saturated | |
| 91 | + Metal GPU compute costs < 5%.</p> | |
| 92 | + ${R.ssdChartSvg(chart)} | |
| 93 | + <a class="more" href="/experiments/expH_ssd_feasibility">Experiment H: hypothesis, method, analysis →</a> | |
| 94 | + </div> | |
| 95 | + <div class="card"> | |
| 96 | + <h2>Research phases</h2> | |
| 97 | + <ul class="phases">${phaseHtml}</ul> | |
| 98 | + </div> | |
| 99 | +</section> | |
| 100 | + | |
| 101 | +<section class="card"> | |
| 102 | + <h2>Latest from the research log</h2> | |
| 103 | + <div class="log-grid">${logHtml}</div> | |
| 104 | +</section>`; | |
| 105 | + page(res, { title: "Home", active: "Home", body }); | |
| 106 | +}); | |
| 107 | + | |
| 108 | +// ---------------------------------------------------------------- research index | |
| 109 | +app.get("/research", (req, res) => { | |
| 110 | + const docs = [ | |
| 111 | + ["research/LOG.md", "Research log", "Append-only, auditable record of every question, experiment, result, and decision."], | |
| 112 | + ["research/state_of_the_art.md", "State of the art", "Phase 2 — technique taxonomy across six families with overlap analysis."], | |
| 113 | + ["research/research_gaps.md", "Research gaps", "Phase 3 — falsifiable approaches with kill-numbers, G01–G24."], | |
| 114 | + ["research/candidate_ranking.md", "Candidate ranking", "Phase 4 — 10-axis scoring and selected prototype candidates."], | |
| 115 | + ["research/bibliography.md", "Bibliography", "Every consulted source with URL and access date."], | |
| 116 | + ["research/novelty_check.md", "Novelty check", "Phase 11 — final novelty verification (written last)."], | |
| 117 | + ]; | |
| 118 | + const cards = docs | |
| 119 | + .map(([rel, title, desc]) => { | |
| 120 | + const ok = C.exists(rel); | |
| 121 | + return `<a class="doc-card ${ok ? "" : "disabled"}" href="${ok ? "/doc/" + rel : "#"}"> | |
| 122 | + <h3>${title}</h3><p>${desc}</p>${ok ? "" : '<span class="badge badge-pending">not yet written</span>'}</a>`; | |
| 123 | + }) | |
| 124 | + .join(""); | |
| 125 | + const notes = (C.listDir("research/notes") || []) | |
| 126 | + .filter((f) => f.name.endsWith(".md") && f.name !== "README.md") | |
| 127 | + .map((f) => { | |
| 128 | + const md = C.readMarkdown(f.rel); | |
| 129 | + const doc = md && md.data.document ? md.data.document : f.name; | |
| 130 | + return `<a class="doc-card" href="/doc/${f.rel}"><h3>${R.esc(f.name.replace(".md", "").replace(/_/g, " "))}</h3> | |
| 131 | + <p class="mono-small">${R.esc(doc)}</p></a>`; | |
| 132 | + }) | |
| 133 | + .join(""); | |
| 134 | + const body = `<h1 class="page-title">Research documents</h1> | |
| 135 | +<div class="doc-grid">${cards}</div> | |
| 136 | +<h2 class="section-title">Phase 1 literature notes</h2> | |
| 137 | +<div class="doc-grid">${notes}</div> | |
| 138 | +<p class="note-line">The project charter itself is public: <a href="/doc/CLAUDE.md">read the full research charter</a>.</p>`; | |
| 139 | + page(res, { title: "Research", active: "Research", body }); | |
| 140 | +}); | |
| 141 | + | |
| 142 | +// ---------------------------------------------------------------- markdown viewer | |
| 143 | +app.get(/^\/doc\/(.+)$/, (req, res) => { | |
| 144 | + const rel = req.params[0]; | |
| 145 | + const md = rel.endsWith(".md") ? C.readMarkdown(rel) : null; | |
| 146 | + if (!md) return notFound(res); | |
| 147 | + const body = `<article class="doc"> | |
| 148 | + <p class="crumb"><a href="/research">Research</a> / ${R.esc(rel)}</p> | |
| 149 | + ${metaChips(md.data)} | |
| 150 | + <div class="md">${R.markdownToHtml(md.content, rel)}</div> | |
| 151 | + </article>`; | |
| 152 | + page(res, { title: md.data.document || rel, active: "Research", body }); | |
| 153 | +}); | |
| 154 | + | |
| 155 | +// ---------------------------------------------------------------- experiments | |
| 156 | +app.get("/experiments", (req, res) => { | |
| 157 | + const exps = C.listExperiments(); | |
| 158 | + const cards = exps | |
| 159 | + .map( | |
| 160 | + (e) => `<a class="doc-card" href="/experiments/${e.id}"> | |
| 161 | + <h3>${R.esc(e.id)}</h3><p>${R.esc(e.purpose)}</p> | |
| 162 | + <span class="badge badge-${e.status === "completed" ? "done" : e.status === "has results" ? "in-progress" : "pending"}">${e.status}</span> | |
| 163 | + ${e.runs ? `<span class="runs">${e.runs} result run${e.runs > 1 ? "s" : ""}</span>` : ""}</a>` | |
| 164 | + ) | |
| 165 | + .join(""); | |
| 166 | + const body = `<h1 class="page-title">Experiments</h1> | |
| 167 | +<p class="lede-small">Every experiment carries a registered hypothesis with an explicit falsification criterion | |
| 168 | +(seven-field scientific block), a benchmark implementation, raw results, and an analysis. Negative results are kept.</p> | |
| 169 | +<div class="doc-grid">${cards}</div>`; | |
| 170 | + page(res, { title: "Experiments", active: "Experiments", body }); | |
| 171 | +}); | |
| 172 | + | |
| 173 | +app.get("/experiments/:id", (req, res) => { | |
| 174 | + const exps = C.listExperiments(); | |
| 175 | + const exp = exps.find((e) => e.id === req.params.id); | |
| 176 | + if (!exp) return notFound(res); | |
| 177 | + const sections = []; | |
| 178 | + for (const [file, title] of [["hypothesis.md", "Hypothesis"], ["analysis.md", "Analysis"], ["README.md", "README"]]) { | |
| 179 | + const md = C.readMarkdown(path.posix.join(exp.rel, file)); | |
| 180 | + if (md && md.content.trim().length > 40) { | |
| 181 | + sections.push(`<section class="card"><h2>${title}</h2>${metaChips(md.data)} | |
| 182 | + <div class="md">${R.markdownToHtml(md.content, path.posix.join(exp.rel, file))}</div></section>`); | |
| 183 | + } | |
| 184 | + } | |
| 185 | + const runs = C.listResultRuns().filter((r) => r.experiment === exp.id); | |
| 186 | + const runsHtml = runs.length | |
| 187 | + ? `<section class="card"><h2>Result runs</h2><ul class="file-list">` + | |
| 188 | + runs.map((r) => r.files.map((f) => `<li><a href="/results/${f.rel}">${r.timestamp} / ${f.name}</a> <span class="mono-small">${(f.size / 1024).toFixed(1)} KiB</span></li>`).join("")).join("") + | |
| 189 | + `</ul></section>` | |
| 190 | + : ""; | |
| 191 | + const codeLink = C.exists(path.posix.join(exp.rel, "benchmark.py")) | |
| 192 | + ? `<p><a class="more" href="/file/${exp.rel}/benchmark.py">View benchmark implementation (benchmark.py) →</a></p>` | |
| 193 | + : ""; | |
| 194 | + const body = `<p class="crumb"><a href="/experiments">Experiments</a> / ${R.esc(exp.id)}</p> | |
| 195 | +<h1 class="page-title">${R.esc(exp.id)}</h1><p class="lede-small">${R.esc(exp.purpose)}</p> | |
| 196 | +${codeLink}${sections.join("")}${runsHtml}`; | |
| 197 | + page(res, { title: exp.id, active: "Experiments", body }); | |
| 198 | +}); | |
| 199 | + | |
| 200 | +// ---------------------------------------------------------------- results | |
| 201 | +app.get("/results", (req, res) => { | |
| 202 | + const runs = C.listResultRuns(); | |
| 203 | + const rows = runs | |
| 204 | + .map( | |
| 205 | + (r) => `<tr><td><a href="/experiments/${r.experiment}">${r.experiment}</a></td> | |
| 206 | + <td class="mono-small">${r.timestamp}</td> | |
| 207 | + <td>${r.files.map((f) => `<a href="/results/${f.rel}">${f.name}</a>`).join(" · ")}</td></tr>` | |
| 208 | + ) | |
| 209 | + .join(""); | |
| 210 | + const body = `<h1 class="page-title">Raw results</h1> | |
| 211 | +<p class="lede-small">Every result is reproducible from commit hash + config + seed + hardware manifest, | |
| 212 | +and each JSON embeds the manifest of the exact machine that produced it.</p> | |
| 213 | +<table class="results-table"><thead><tr><th>Experiment</th><th>Run (UTC)</th><th>Files</th></tr></thead> | |
| 214 | +<tbody>${rows || '<tr><td colspan="3">No result runs yet.</td></tr>'}</tbody></table>`; | |
| 215 | + page(res, { title: "Results", active: "Results", body }); | |
| 216 | +}); | |
| 217 | + | |
| 218 | +app.get(/^\/results\/(.+)$/, (req, res) => { | |
| 219 | + const rel = req.params[0].startsWith("results/") ? req.params[0] : "results/" + req.params[0]; | |
| 220 | + const raw = C.readText(rel); | |
| 221 | + if (raw === null) return notFound(res); | |
| 222 | + let bodyContent; | |
| 223 | + if (rel.endsWith(".json")) { | |
| 224 | + const obj = C.readJson(rel); | |
| 225 | + bodyContent = `<pre class="codeblock"><code class="hljs">${R.highlightFile(JSON.stringify(obj, null, 2), "x.json")}</code></pre>`; | |
| 226 | + } else { | |
| 227 | + bodyContent = `<pre class="codeblock"><code>${R.esc(raw.slice(0, 200000))}</code></pre>`; | |
| 228 | + } | |
| 229 | + const body = `<p class="crumb"><a href="/results">Results</a> / ${R.esc(rel)}</p><div class="card">${bodyContent}</div>`; | |
| 230 | + page(res, { title: rel, active: "Results", body }); | |
| 231 | +}); | |
| 232 | + | |
| 233 | +// ---------------------------------------------------------------- code browser | |
| 234 | +const CODE_ROOTS = ["src", "tools", "benchmarks", "experiments", "Makefile", "pyproject.toml", "CITATION.cff"]; | |
| 235 | +app.get("/code", (req, res) => { | |
| 236 | + const sections = CODE_ROOTS.map((root) => { | |
| 237 | + if (!C.exists(root)) return ""; | |
| 238 | + const st = C.listDir(root); | |
| 239 | + if (st === null) { | |
| 240 | + return `<li><a href="/file/${root}">${root}</a></li>`; | |
| 241 | + } | |
| 242 | + const files = C.walk(root, (f) => C.isTextFile(f.rel) && !f.rel.includes("results/")); | |
| 243 | + return `<li class="tree-root"><strong>${root}/</strong><ul>` + | |
| 244 | + files.map((f) => `<li><a href="/file/${f.rel}">${f.rel.slice(root.length + 1)}</a></li>`).join("") + | |
| 245 | + `</ul></li>`; | |
| 246 | + }).join(""); | |
| 247 | + const body = `<h1 class="page-title">Code</h1> | |
| 248 | +<p class="lede-small">Core library (<code>src/localvm/</code>), tooling, benchmark harness, and experiment | |
| 249 | +implementations. Every file carries the project's author header; MLX/Metal is the primary compute path — CUDA | |
| 250 | +is never a core dependency.</p> | |
| 251 | +<ul class="tree">${sections}</ul>`; | |
| 252 | + page(res, { title: "Code", active: "Code", body }); | |
| 253 | +}); | |
| 254 | + | |
| 255 | +app.get(/^\/file\/(.+)$/, (req, res) => { | |
| 256 | + const rel = req.params[0]; | |
| 257 | + if (!C.isTextFile(rel)) return notFound(res); | |
| 258 | + const raw = C.readText(rel); | |
| 259 | + if (raw === null) return notFound(res); | |
| 260 | + const name = rel.split("/").pop(); | |
| 261 | + const body = `<p class="crumb"><a href="/code">Code</a> / ${R.esc(rel)}</p> | |
| 262 | +<div class="card file-card"><div class="file-head"><span class="mono-small">${R.esc(rel)}</span> | |
| 263 | +<span class="mono-small">${raw.split("\n").length} lines</span></div> | |
| 264 | +<pre class="codeblock"><code class="hljs">${R.highlightFile(raw.slice(0, 400000), name)}</code></pre></div>`; | |
| 265 | + page(res, { title: name, active: "Code", body }); | |
| 266 | +}); | |
| 267 | + | |
| 268 | +// ---------------------------------------------------------------- about | |
| 269 | +app.get("/about", (req, res) => { | |
| 270 | + const chart = C.expHChartData(); | |
| 271 | + const m = (chart && chart.manifest) || {}; | |
| 272 | + const chip = m.chip || {}, mem = m.memory || {}, ssd = m.ssd || {}, os = m.os || {}, sw = m.software || {}; | |
| 273 | + const hw = ` | |
| 274 | +<table class="kv-table"> | |
| 275 | +<tr><th>Chip</th><td>${R.esc(chip.brand || "Apple M5 Max")} — ${chip.cores_performance || 6}P + ${chip.cores_efficiency || 12}E CPU cores, ${chip.gpu_cores || 40} GPU cores</td></tr> | |
| 276 | +<tr><th>Unified memory</th><td>${mem.unified_gb || 48} GB (16 KiB pages)</td></tr> | |
| 277 | +<tr><th>Storage</th><td>${R.esc(ssd.model || "APPLE SSD AP2048Z")} ${R.esc(ssd.size || "2 TB")} — measured ceiling ≈ 13.1 GB/s (iostat-validated)</td></tr> | |
| 278 | +<tr><th>OS</th><td>macOS ${R.esc(os.version || "27.0")} (${R.esc(os.build || "")})</td></tr> | |
| 279 | +<tr><th>Stack</th><td>Python ${R.esc(sw.python || "3.14")}, MLX ${R.esc(sw.mlx || "0.32")}, PyTorch ${R.esc(sw.torch || "2.12")} (MPS), Metal</td></tr> | |
| 280 | +</table>`; | |
| 281 | + const body = `<h1 class="page-title">About this project</h1> | |
| 282 | +<div class="card md"> | |
| 283 | +<p><strong>localvm-research</strong> is an independent research project by | |
| 284 | +<strong>Simon-Pierre Boucher</strong> (<a href="mailto:contact@spboucher.ai">contact@spboucher.ai</a>) investigating | |
| 285 | +whether pretrained large language models that normally do not fit in a consumer Mac's memory can be transformed — | |
| 286 | +strictly <em>post-training</em> — into execution representations with dramatically smaller instantaneous working sets.</p> | |
| 287 | +<p>The methodology is deliberately strict: an append-only research log, registered hypotheses with explicit | |
| 288 | +falsification criteria before any experiment runs, no results reported from uncommitted code, hardware manifests | |
| 289 | +embedded in every result file, and negative results kept and published. The full methodology is codified in the | |
| 290 | +<a href="/doc/CLAUDE.md">research charter</a>.</p> | |
| 291 | +<h2>Primary research hardware</h2>${hw} | |
| 292 | +<h2>What would count as a breakthrough</h2> | |
| 293 | +<ul> | |
| 294 | +<li>A model significantly larger than unified memory running locally with acceptable interactive latency.</li> | |
| 295 | +<li>Bytes transferred per token substantially smaller than the compressed checkpoint.</li> | |
| 296 | +<li>Evidence that only a small token-dependent fraction of model information is required during typical inference.</li> | |
| 297 | +<li>A progressive/conditional execution mechanism that preserves quality while avoiding most weight loading.</li> | |
| 298 | +<li>A post-training representation with a qualitatively better storage/RAM/quality tradeoff than fixed quantization.</li> | |
| 299 | +</ul> | |
| 300 | +<p>Failure is an acceptable outcome — the project's charter defines explicit failure criteria, and the log records | |
| 301 | +why an approach died, not just what survived.</p> | |
| 302 | +</div>`; | |
| 303 | + page(res, { title: "About", active: "About", body }); | |
| 304 | +}); | |
| 305 | + | |
| 306 | +// ---------------------------------------------------------------- misc | |
| 307 | +app.get("/health", (req, res) => res.json({ ok: true, app: "localvm-web", author: "Simon-Pierre Boucher" })); | |
| 308 | + | |
| 309 | +function notFound(res) { | |
| 310 | + res.status(404); | |
| 311 | + page(res, { title: "Not found", active: "", body: `<div class="card"><h1>404</h1><p>That page does not exist. <a href="/">Back to home</a>.</p></div>` }); | |
| 312 | +} | |
| 313 | +app.use((req, res) => notFound(res)); | |
| 314 | + | |
| 315 | +app.listen(PORT, () => console.log(`localvm-web listening on :${PORT}`)); | |
added
web/sync-content.sh
+39 −0
@@ -0,0 +1,39 @@ | ||
| 1 | +#!/bin/zsh | |
| 2 | +# ============================================================================= | |
| 3 | +# Project : localvm-research | |
| 4 | +# File : web/sync-content.sh | |
| 5 | +# Purpose : Snapshot the research repo into web/content/ + build-info.json | |
| 6 | +# Author : Simon-Pierre Boucher | |
| 7 | +# Contact : contact@spboucher.ai | |
| 8 | +# Created : 2026-08-12 | |
| 9 | +# Modified : 2026-08-12 | |
| 10 | +# Platform : macOS / Apple Silicon (arm64) | |
| 11 | +# License : All rights reserved (research code) | |
| 12 | +# ============================================================================= | |
| 13 | +set -euo pipefail | |
| 14 | + | |
| 15 | +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" | |
| 16 | +CONTENT="$REPO_ROOT/web/content" | |
| 17 | + | |
| 18 | +mkdir -p "$CONTENT" | |
| 19 | +rsync -a --delete \ | |
| 20 | + --include='CLAUDE.md' --include='README.md' --include='CITATION.cff' \ | |
| 21 | + --include='Makefile' --include='pyproject.toml' \ | |
| 22 | + --include='research/***' --include='experiments/***' --include='benchmarks/***' \ | |
| 23 | + --include='src/***' --include='tools/***' --include='results/***' --include='docs/***' \ | |
| 24 | + --exclude='*.bin' --exclude='*.log' --exclude='__pycache__' --exclude='.git' \ | |
| 25 | + --exclude='node_modules' --exclude='web' --exclude='*' \ | |
| 26 | + "$REPO_ROOT/" "$CONTENT/" | |
| 27 | + | |
| 28 | +COMMIT=$(git -C "$REPO_ROOT" rev-parse HEAD 2>/dev/null || echo "unknown") | |
| 29 | +COMMITS=$(git -C "$REPO_ROOT" rev-list --count HEAD 2>/dev/null || echo 0) | |
| 30 | +cat > "$CONTENT/build-info.json" <<EOF | |
| 31 | +{ | |
| 32 | + "author": "Simon-Pierre Boucher", | |
| 33 | + "contact": "contact@spboucher.ai", | |
| 34 | + "commit": "$COMMIT", | |
| 35 | + "commits": $COMMITS, | |
| 36 | + "generated": "$(date -u +%Y-%m-%dT%H:%M:%SZ)" | |
| 37 | +} | |
| 38 | +EOF | |
| 39 | +echo "content synced at commit ${COMMIT:0:7} ($COMMITS commits)" | |
| 40 | ||