SPB Git

spb/localvm-research Public License

Running LLMs larger than memory on a consumer Mac — falsification-driven research: margin-gated deferred refinement, out-of-core verification on Apple Silicon. TR-01 published.

Python 63.2% JavaScript 23.5% CSS 11.8% Shell 0.9% Makefile 0.5%

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>
Simon-Pierre Boucher committed 6 h ago (Aug 12, 2026) parent 78a6b1e

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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[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 &lt; 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