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: professional document display + smartphone adaptation

- Research log rendered as a timeline (date rail, entry cards)
- Long docs get sticky table of contents + heading anchors
- Document headers from front matter (title, status badge, dates)
- Notes cards with source counts; fixed marked.use() handler stacking
- Full mobile pass: scrollable nav, 2-col tiles, collapsing TOC,
  overflow-x tables/chart, type scale

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed 5 h ago (Aug 12, 2026) parent 999818d

Showing 6 changed files with +759 and −30

added experiments/micro/expA_weight_concentration/expA_full.log +12 −0
@@ -0,0 +1,12 @@
1 +loading mlx-community/Qwen3-1.7B-bf16 …
2 + Fetching 9 files: 0%| | 0/9 [00:00<?, ?it/s] Fetching 9 files: 100%|██████████| 9/9 [00:00<00:00, 5740.38it/s]
3 +48 trajectories in 48s
4 + traced 12/48
5 + traced 24/48
6 + traced 36/48
7 + traced 48/48
8 +trace shape (6144, 28, 384)
9 +
10 +wrote /Users/simon-pierreboucher/Desktop/localvm-research/results/expA_weight_concentration/20260812T044829Z/results.json (+ block_energy_trace.npz for expB)
11 +overall block-64: {"top_frac_energy": {"0.1": 0.5811625123023987, "0.2": 0.7119550108909607, "0.4": 0.8502882122993469, "0.6": 0.9268651604652405}, "frac_needed_for": {"0.9": 0.47826662517729257, "0.95": 0.6121286816067165, "0.99": 0.8104762122744605}}
12 +neuron-level : {"top_frac_energy": {"0.1": 0.8779218658982288, "0.2": 0.9387342477483409, "0.4": 0.9827663398658236, "0.6": 0.9962594589839379}, "frac_needed_for": {"0.9": 0.12640662041921463, "0.95": 0.1993891048052954, "0.99": 0.37685486532392953}}
added results/expA_weight_concentration/20260812T044829Z/block_energy_trace.npz +0 −0

Binary file not shown.

added results/expA_weight_concentration/20260812T044829Z/results.json +546 −0
@@ -0,0 +1,546 @@
1 +{
2 + "experiment": "expA_weight_concentration",
3 + "author": "Simon-Pierre Boucher",
4 + "contact": "contact@spboucher.ai",
5 + "manifest": {
6 + "author": "Simon-Pierre Boucher",
7 + "contact": "contact@spboucher.ai",
8 + "project": "localvm-research",
9 + "collected_utc": "2026-08-12T04:48:32.303830+00:00",
10 + "chip": {
11 + "brand": "Apple M5 Max",
12 + "arch": "arm64",
13 + "cores_total": 18,
14 + "cores_performance": 6,
15 + "cores_efficiency": 12,
16 + "gpu_cores": 40
17 + },
18 + "memory": {
19 + "unified_bytes": 51539607552,
20 + "unified_gb": 48.0,
21 + "pagesize": 16384
22 + },
23 + "ssd": {
24 + "model": "APPLE SSD AP2048Z",
25 + "size": "2 TB",
26 + "smart_status": "Verified"
27 + },
28 + "os": {
29 + "product": "macOS",
30 + "version": "27.0",
31 + "build": "26A5388g",
32 + "kernel": "27.0.0"
33 + },
34 + "software": {
35 + "python": "3.14.4",
36 + "mlx": "0.32.0",
37 + "mlx_lm": "0.31.3",
38 + "torch": null,
39 + "numpy": "2.5.2"
40 + },
41 + "git": {
42 + "commit": "999818d428ca32f72b87bcdede60b7fb3d70f99c",
43 + "dirty_tree": true
44 + },
45 + "thermal_level_at_collect": null
46 + },
47 + "config": {
48 + "model": "mlx-community/Qwen3-1.7B-bf16",
49 + "gen_tokens": 128,
50 + "per_domain": 8,
51 + "block": 16
52 + },
53 + "n_positions": 6144,
54 + "n_layers": 28,
55 + "n_blocks": 384,
56 + "coarse_block_granularity": {
57 + "block_size": 64,
58 + "overall": {
59 + "top_frac_energy": {
60 + "0.1": 0.43653321266174316,
61 + "0.2": 0.5667904019355774,
62 + "0.4": 0.7417362928390503,
63 + "0.6": 0.8644383549690247
64 + },
65 + "frac_needed_for": {
66 + "0.9": 0.6482662019275484,
67 + "0.95": 0.7711266411675348,
68 + "0.99": 0.918917943560888
69 + }
70 + }
71 + },
72 + "block_granularity": {
73 + "overall": {
74 + "top_frac_energy": {
75 + "0.1": 0.5811625123023987,
76 + "0.2": 0.7119550108909607,
77 + "0.4": 0.8502882122993469,
78 + "0.6": 0.9268651604652405
79 + },
80 + "frac_needed_for": {
81 + "0.9": 0.47826662517729257,
82 + "0.95": 0.6121286816067165,
83 + "0.99": 0.8104762122744605
84 + }
85 + },
86 + "per_layer": {
87 + "0": {
88 + "top_frac_energy": {
89 + "0.1": 0.6874333024024963,
90 + "0.2": 0.7650604844093323,
91 + "0.4": 0.8619165420532227,
92 + "0.6": 0.926844596862793
93 + },
94 + "frac_needed_for": {
95 + "0.9": 0.4870702955457899,
96 + "0.95": 0.6730901930067273,
97 + "0.99": 0.8978864881727432
98 + }
99 + },
100 + "1": {
101 + "top_frac_energy": {
102 + "0.1": 0.613547146320343,
103 + "0.2": 0.7242605090141296,
104 + "0.4": 0.854881227016449,
105 + "0.6": 0.9318239092826843
106 + },
107 + "frac_needed_for": {
108 + "0.9": 0.4837188720703125,
109 + "0.95": 0.6426866319444445,
110 + "0.99": 0.8656713697645401
111 + }
112 + },
113 + "2": {
114 + "top_frac_energy": {
115 + "0.1": 0.5386462807655334,
116 + "0.2": 0.6579890847206116,
117 + "0.4": 0.8067877888679504,
118 + "0.6": 0.9021837711334229
119 + },
120 + "frac_needed_for": {
121 + "0.9": 0.5812462700737847,
122 + "0.95": 0.7296748691134982,
123 + "0.99": 0.9113070170084635
124 + }
125 + },
126 + "3": {
127 + "top_frac_energy": {
128 + "0.1": 0.5703957676887512,
129 + "0.2": 0.6802437901496887,
130 + "0.4": 0.8173820376396179,
131 + "0.6": 0.9061954021453857
132 + },
133 + "frac_needed_for": {
134 + "0.9": 0.5644103156195747,
135 + "0.95": 0.7199134826660156,
136 + "0.99": 0.9095013936360677
137 + }
138 + },
139 + "4": {
140 + "top_frac_energy": {
141 + "0.1": 0.5719252228736877,
142 + "0.2": 0.6677220463752747,
143 + "0.4": 0.7978194355964661,
144 + "0.6": 0.8898757100105286
145 + },
146 + "frac_needed_for": {
147 + "0.9": 0.6015315585666233,
148 + "0.95": 0.7550129360622829,
149 + "0.99": 0.9276212056477865
150 + }
151 + },
152 + "5": {
153 + "top_frac_energy": {
154 + "0.1": 0.4694152772426605,
155 + "0.2": 0.5891006588935852,
156 + "0.4": 0.7470541000366211,
157 + "0.6": 0.8598141670227051
158 + },
159 + "frac_needed_for": {
160 + "0.9": 0.6790432400173612,
161 + "0.95": 0.8086844550238715,
162 + "0.99": 0.946531507703993
163 + }
164 + },
165 + "6": {
166 + "top_frac_energy": {
167 + "0.1": 0.4781348705291748,
168 + "0.2": 0.599266767501831,
169 + "0.4": 0.7550390362739563,
170 + "0.6": 0.8647563457489014
171 + },
172 + "frac_needed_for": {
173 + "0.9": 0.6704851786295573,
174 + "0.95": 0.8029717339409723,
175 + "0.99": 0.9446830749511719
176 + }
177 + },
178 + "7": {
179 + "top_frac_energy": {
180 + "0.1": 0.48662224411964417,
181 + "0.2": 0.6025902628898621,
182 + "0.4": 0.7557342648506165,
183 + "0.6": 0.8648033142089844
184 + },
185 + "frac_needed_for": {
186 + "0.9": 0.6610705057779948,
187 + "0.95": 0.795810275607639,
188 + "0.99": 0.9423624674479166
189 + }
190 + },
191 + "8": {
192 + "top_frac_energy": {
193 + "0.1": 0.47597572207450867,
194 + "0.2": 0.6017529964447021,
195 + "0.4": 0.7585147023200989,
196 + "0.6": 0.8671410083770752
197 + },
198 + "frac_needed_for": {
199 + "0.9": 0.664475335015191,
200 + "0.95": 0.7988082038031684,
201 + "0.99": 0.9433424207899305
202 + }
203 + },
204 + "9": {
205 + "top_frac_energy": {
206 + "0.1": 0.5099363923072815,
207 + "0.2": 0.6356163620948792,
208 + "0.4": 0.7846576571464539,
209 + "0.6": 0.883351743221283
210 + },
211 + "frac_needed_for": {
212 + "0.9": 0.6280971103244358,
213 + "0.95": 0.7733629014756945,
214 + "0.99": 0.9348063998752171
215 + }
216 + },
217 + "10": {
218 + "top_frac_energy": {
219 + "0.1": 0.48266366124153137,
220 + "0.2": 0.6276065707206726,
221 + "0.4": 0.7905141711235046,
222 + "0.6": 0.8904873728752136
223 + },
224 + "frac_needed_for": {
225 + "0.9": 0.6153797573513454,
226 + "0.95": 0.7605523003472223,
227 + "0.99": 0.9287876553005643
228 + }
229 + },
230 + "11": {
231 + "top_frac_energy": {
232 + "0.1": 0.45536351203918457,
233 + "0.2": 0.6070888638496399,
234 + "0.4": 0.7831881642341614,
235 + "0.6": 0.8892932534217834
236 + },
237 + "frac_needed_for": {
238 + "0.9": 0.6168941921657985,
239 + "0.95": 0.7590213351779513,
240 + "0.99": 0.9268175760904948
241 + }
242 + },
243 + "12": {
244 + "top_frac_energy": {
245 + "0.1": 0.42641258239746094,
246 + "0.2": 0.5898794531822205,
247 + "0.4": 0.78082674741745,
248 + "0.6": 0.8923371434211731
249 + },
250 + "frac_needed_for": {
251 + "0.9": 0.6151207817925347,
252 + "0.95": 0.7540804545084635,
253 + "0.99": 0.9232838948567709
254 + }
255 + },
256 + "13": {
257 + "top_frac_energy": {
258 + "0.1": 0.4309774339199066,
259 + "0.2": 0.5965408086776733,
260 + "0.4": 0.7884756922721863,
261 + "0.6": 0.898248016834259
262 + },
263 + "frac_needed_for": {
264 + "0.9": 0.6022063361273872,
265 + "0.95": 0.7422955830891927,
266 + "0.99": 0.9176712036132812
267 + }
268 + },
269 + "14": {
270 + "top_frac_energy": {
271 + "0.1": 0.44317904114723206,
272 + "0.2": 0.609806478023529,
273 + "0.4": 0.7995373606681824,
274 + "0.6": 0.9057241082191467
275 + },
276 + "frac_needed_for": {
277 + "0.9": 0.5839491950141059,
278 + "0.95": 0.7258415222167969,
279 + "0.99": 0.9094590081108942
280 + }
281 + },
282 + "15": {
283 + "top_frac_energy": {
284 + "0.1": 0.43841782212257385,
285 + "0.2": 0.607620894908905,
286 + "0.4": 0.8003490567207336,
287 + "0.6": 0.9074628949165344
288 + },
289 + "frac_needed_for": {
290 + "0.9": 0.5802201165093316,
291 + "0.95": 0.7204712761773004,
292 + "0.99": 0.9054489135742188
293 + }
294 + },
295 + "16": {
296 + "top_frac_energy": {
297 + "0.1": 0.45128610730171204,
298 + "0.2": 0.6225364208221436,
299 + "0.4": 0.8147709965705872,
300 + "0.6": 0.9181920886039734
301 + },
302 + "frac_needed_for": {
303 + "0.9": 0.553299797905816,
304 + "0.95": 0.6920729743109808,
305 + "0.99": 0.8864991929796006
306 + }
307 + },
308 + "17": {
309 + "top_frac_energy": {
310 + "0.1": 0.5729883313179016,
311 + "0.2": 0.7198475003242493,
312 + "0.4": 0.8696538805961609,
313 + "0.6": 0.9440474510192871
314 + },
315 + "frac_needed_for": {
316 + "0.9": 0.4632394578721788,
317 + "0.95": 0.6179097493489584,
318 + "0.99": 0.8542548285590277
319 + }
320 + },
321 + "18": {
322 + "top_frac_energy": {
323 + "0.1": 0.5495641827583313,
324 + "0.2": 0.7102975845336914,
325 + "0.4": 0.8709120154380798,
326 + "0.6": 0.9468779563903809
327 + },
328 + "frac_needed_for": {
329 + "0.9": 0.45826043023003465,
330 + "0.95": 0.6066763136121961,
331 + "0.99": 0.8438733418782552
332 + }
333 + },
334 + "19": {
335 + "top_frac_energy": {
336 + "0.1": 0.5876293778419495,
337 + "0.2": 0.7428757548332214,
338 + "0.4": 0.8937171101570129,
339 + "0.6": 0.9606165885925293
340 + },
341 + "frac_needed_for": {
342 + "0.9": 0.41054577297634554,
343 + "0.95": 0.5506146748860677,
344 + "0.99": 0.793799082438151
345 + }
346 + },
347 + "20": {
348 + "top_frac_energy": {
349 + "0.1": 0.6525656580924988,
350 + "0.2": 0.7988567352294922,
351 + "0.4": 0.9251458048820496,
352 + "0.6": 0.974200963973999
353 + },
354 + "frac_needed_for": {
355 + "0.9": 0.33748541937934023,
356 + "0.95": 0.47205437554253465,
357 + "0.99": 0.7357359992133246
358 + }
359 + },
360 + "21": {
361 + "top_frac_energy": {
362 + "0.1": 0.6976020336151123,
363 + "0.2": 0.8338201642036438,
364 + "0.4": 0.9442384839057922,
365 + "0.6": 0.9827890992164612
366 + },
367 + "frac_needed_for": {
368 + "0.9": 0.2885225084092882,
369 + "0.95": 0.41179741753472215,
370 + "0.99": 0.6691720750596789
371 + }
372 + },
373 + "22": {
374 + "top_frac_energy": {
375 + "0.1": 0.7465164065361023,
376 + "0.2": 0.8712833523750305,
377 + "0.4": 0.961615264415741,
378 + "0.6": 0.9888022541999817
379 + },
380 + "frac_needed_for": {
381 + "0.9": 0.23829311794704863,
382 + "0.95": 0.35068511962890625,
383 + "0.99": 0.6056221856011285
384 + }
385 + },
386 + "23": {
387 + "top_frac_energy": {
388 + "0.1": 0.7757749557495117,
389 + "0.2": 0.8957595825195312,
390 + "0.4": 0.972876250743866,
391 + "0.6": 0.9925090670585632
392 + },
393 + "frac_needed_for": {
394 + "0.9": 0.20180977715386286,
395 + "0.95": 0.29830296834309894,
396 + "0.99": 0.5363205803765191
397 + }
398 + },
399 + "24": {
400 + "top_frac_energy": {
401 + "0.1": 0.774891197681427,
402 + "0.2": 0.8894092440605164,
403 + "0.4": 0.9688065648078918,
404 + "0.6": 0.9915411472320557
405 + },
406 + "frac_needed_for": {
407 + "0.9": 0.21235232883029517,
408 + "0.95": 0.31633673773871523,
409 + "0.99": 0.5550952487521701
410 + }
411 + },
412 + "25": {
413 + "top_frac_energy": {
414 + "0.1": 0.7099928259849548,
415 + "0.2": 0.848381757736206,
416 + "0.4": 0.9529193043708801,
417 + "0.6": 0.9868856072425842
418 + },
419 + "frac_needed_for": {
420 + "0.9": 0.267195807562934,
421 + "0.95": 0.38037872314453125,
422 + "0.99": 0.6180953979492188
423 + }
424 + },
425 + "26": {
426 + "top_frac_energy": {
427 + "0.1": 0.7293014526367188,
428 + "0.2": 0.8571066856384277,
429 + "0.4": 0.9542860984802246,
430 + "0.6": 0.9863548278808594
431 + },
432 + "frac_needed_for": {
433 + "0.9": 0.25812700059678817,
434 + "0.95": 0.37402809990776903,
435 + "0.99": 0.6243955824110242
436 + }
437 + },
438 + "27": {
439 + "top_frac_energy": {
440 + "0.1": 0.9453914165496826,
441 + "0.2": 0.9824205040931702,
442 + "0.4": 0.9964491724967957,
443 + "0.6": 0.9990647435188293
444 + },
445 + "frac_needed_for": {
446 + "0.9": 0.06741502549913193,
447 + "0.95": 0.106467776828342,
448 + "0.99": 0.23528883192274308
449 + }
450 + }
451 + },
452 + "per_domain": {
453 + "code": {
454 + "top_frac_energy": {
455 + "0.1": 0.5819565057754517,
456 + "0.2": 0.7126027941703796,
457 + "0.4": 0.8507959246635437,
458 + "0.6": 0.9272078275680542
459 + },
460 + "frac_needed_for": {
461 + "0.9": 0.47715495881580167,
462 + "0.95": 0.6113315763927643,
463 + "0.99": 0.8103377932593936
464 + }
465 + },
466 + "math": {
467 + "top_frac_energy": {
468 + "0.1": 0.588861346244812,
469 + "0.2": 0.7172813415527344,
470 + "0.4": 0.8528871536254883,
471 + "0.6": 0.9280996322631836
472 + },
473 + "frac_needed_for": {
474 + "0.9": 0.4730159214564732,
475 + "0.95": 0.6073925381615048,
476 + "0.99": 0.8076715923490979
477 + }
478 + },
479 + "chat": {
480 + "top_frac_energy": {
481 + "0.1": 0.5798209309577942,
482 + "0.2": 0.7117646336555481,
483 + "0.4": 0.8507514595985413,
484 + "0.6": 0.9272485971450806
485 + },
486 + "frac_needed_for": {
487 + "0.9": 0.4793227059500558,
488 + "0.95": 0.613118671235584,
489 + "0.99": 0.8100371587844121
490 + }
491 + },
492 + "french": {
493 + "top_frac_energy": {
494 + "0.1": 0.5824114680290222,
495 + "0.2": 0.7130373120307922,
496 + "0.4": 0.8508443236351013,
497 + "0.6": 0.9270651936531067
498 + },
499 + "frac_needed_for": {
500 + "0.9": 0.4778752099900018,
501 + "0.95": 0.611899875459217,
502 + "0.99": 0.810279301234654
503 + }
504 + },
505 + "english": {
506 + "top_frac_energy": {
507 + "0.1": 0.579283595085144,
508 + "0.2": 0.7110149264335632,
509 + "0.4": 0.8503116369247437,
510 + "0.6": 0.9270928502082825
511 + },
512 + "frac_needed_for": {
513 + "0.9": 0.4792231605166481,
514 + "0.95": 0.6137539091564361,
515 + "0.99": 0.8124092647007534
516 + }
517 + },
518 + "reasoning": {
519 + "top_frac_energy": {
520 + "0.1": 0.5746413469314575,
521 + "0.2": 0.7060293555259705,
522 + "0.4": 0.8461384177207947,
523 + "0.6": 0.9244770407676697
524 + },
525 + "frac_needed_for": {
526 + "0.9": 0.4830077943347749,
527 + "0.95": 0.6152755192347935,
528 + "0.99": 0.8121221633184523
529 + }
530 + }
531 + }
532 + },
533 + "neuron_granularity": {
534 + "top_frac_energy": {
535 + "0.1": 0.8779218658982288,
536 + "0.2": 0.9387342477483409,
537 + "0.4": 0.9827663398658236,
538 + "0.6": 0.9962594589839379
539 + },
540 + "frac_needed_for": {
541 + "0.9": 0.12640662041921463,
542 + "0.95": 0.1993891048052954,
543 + "0.99": 0.37685486532392953
544 + }
545 + }
546 +}
\ No newline at end of file
modified web/lib/render.js +80 −16
@@ -11,28 +11,43 @@
11 11 // ============================================================================
12 12 "use strict";
13 13
14 const { marked } = require("marked");
14 +const { Marked } = require("marked");
15 15 const hljs = require("highlight.js");
16 16
17 17 function esc(s) {
18 18 return String(s).replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
19 19 }
20 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 };
21 +function slugify(s) {
22 + return s.toLowerCase().replace(/[^\w\s-]/g, "").trim().replace(/\s+/g, "-").slice(0, 80);
23 +}
32 24
25 +/**
26 + * Render markdown → { html, toc }. A fresh Marked instance per call (a shared
27 + * global `marked.use()` would stack walkTokens handlers on every render).
28 + * Headings get stable anchor ids collected into a table of contents.
29 + */
33 30 function markdownToHtml(md, baseRel) {
34 marked.use({
35 renderer,
31 + const toc = [];
32 + const seen = new Set();
33 + const m = new Marked({
34 + renderer: {
35 + code({ text, lang }) {
36 + const html = lang && hljs.getLanguage(lang)
37 + ? hljs.highlight(text, { language: lang }).value
38 + : esc(text);
39 + return `<pre class="codeblock"><code class="hljs">${html}</code></pre>`;
40 + },
41 + heading({ tokens, depth }) {
42 + const html = this.parser.parseInline(tokens);
43 + const plain = html.replace(/<[^>]+>/g, "");
44 + let id = slugify(plain) || "section";
45 + while (seen.has(id)) id += "-x";
46 + seen.add(id);
47 + if (depth >= 2 && depth <= 3) toc.push({ depth, id, text: plain });
48 + return `<h${depth} id="${id}">${html}<a class="hanchor" href="#${id}" aria-label="Link to section">#</a></h${depth}>\n`;
49 + },
50 + },
36 51 walkTokens(token) {
37 52 if (token.type === "link" && token.href && !/^(https?:|mailto:|#|\/)/.test(token.href)) {
38 53 const base = baseRel.includes("/") ? baseRel.slice(0, baseRel.lastIndexOf("/")) : "";
@@ -46,7 +61,56 @@ function markdownToHtml(md, baseRel) {
46 61 }
47 62 },
48 63 });
49 return marked.parse(md);
64 + return { html: m.parse(md), toc };
65 +}
66 +
67 +/** Sticky table-of-contents sidebar for long documents. */
68 +function tocHtml(toc) {
69 + if (!toc || toc.length < 3) return "";
70 + const items = toc
71 + .map((t) => `<li class="toc-d${t.depth}"><a href="#${t.id}">${esc(t.text)}</a></li>`)
72 + .join("");
73 + return `<nav class="toc" aria-label="Table of contents"><span class="toc-title">On this page</span><ul>${items}</ul></nav>`;
74 +}
75 +
76 +/** Professional document header from front matter. */
77 +function docHeader(data, rel) {
78 + const title = (data.document || rel).split("/").pop().replace(/[_-]/g, " ");
79 + const status = data.status || "draft";
80 + const meta = [
81 + data.created ? `created ${data.created}` : null,
82 + data.modified ? `updated ${data.modified}` : null,
83 + data.author || null,
84 + ].filter(Boolean).join(" · ");
85 + return `<header class="doc-head">
86 + <div class="doc-head-row">
87 + <h1>${esc(title)}</h1>
88 + <span class="badge badge-${status === "reviewed" || status === "final" ? "done" : "in-progress"}">${esc(status)}</span>
89 + </div>
90 + <p class="doc-head-meta">${esc(data.document || rel)}${meta ? " · " + esc(meta) : ""}</p>
91 + </header>`;
92 +}
93 +
94 +/** Research-log timeline: entries already split by content.parseLogEntries. */
95 +function logTimeline(entries, baseRel) {
96 + const items = entries
97 + .slice()
98 + .reverse()
99 + .map((e) => {
100 + const [when, ...rest] = e.title.split(" — ");
101 + const title = rest.join(" — ") || when;
102 + const body = markdownToHtml(e.body, baseRel).html;
103 + return `<article class="tl-entry">
104 + <div class="tl-rail"><span class="tl-dot"></span></div>
105 + <div class="tl-card">
106 + <time class="tl-time">${esc(rest.length ? when : "")}</time>
107 + <h3>${esc(title)}</h3>
108 + <div class="md md-compact">${body}</div>
109 + </div>
110 + </article>`;
111 + })
112 + .join("");
113 + return `<div class="timeline">${items}</div>`;
50 114 }
51 115
52 116 function highlightFile(text, filename) {
@@ -160,4 +224,4 @@ ${g}${xticks}${lines}${dots}${labels}
160 224 </figure>`;
161 225 }
162 226
163 module.exports = { esc, markdownToHtml, highlightFile, layout, ssdChartSvg };
227 +module.exports = { esc, markdownToHtml, tocHtml, docHeader, logTimeline, highlightFile, layout, ssdChartSvg };
modified web/public/style.css +87 −0
@@ -206,3 +206,90 @@ code, pre, .mono-small { font-family: ui-monospace, SFMono-Regular, Menlo, monos
206 206 }
207 207 .site-footer .wrap { display: flex; justify-content: space-between; flex-wrap: wrap; gap: 8px; }
208 208 .footer-meta { color: var(--muted); }
209 +
210 +/* ---- professional document view: header, TOC, anchors -------------------- */
211 +.doc-head { margin: 28px 0 6px; }
212 +.doc-head-row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
213 +.doc-head h1 { font-size: 30px; letter-spacing: -0.02em; margin: 0; text-transform: none; }
214 +.doc-head h1::first-letter { text-transform: uppercase; }
215 +.doc-head-meta { color: var(--muted); font-size: 13px; margin: 6px 0 0; font-family: ui-monospace, Menlo, monospace; }
216 +.doc-layout { display: block; margin-top: 18px; }
217 +.doc-layout.has-toc { display: grid; grid-template-columns: 240px minmax(0, 1fr); gap: 32px; align-items: start; }
218 +.doc-layout .doc { margin: 0; max-width: 820px; }
219 +.toc {
220 + position: sticky; top: 76px; background: var(--surface); border: 1px solid var(--grid);
221 + border-radius: 10px; padding: 14px 16px; max-height: calc(100vh - 100px); overflow-y: auto;
222 +}
223 +.toc-title { font-size: 11px; text-transform: uppercase; letter-spacing: 0.07em; color: var(--muted); font-weight: 600; }
224 +.toc ul { list-style: none; margin: 8px 0 0; padding: 0; }
225 +.toc li { padding: 3px 0; line-height: 1.35; }
226 +.toc li a { color: var(--ink-2); font-size: 13px; display: block; }
227 +.toc li a:hover { color: var(--accent-dark); text-decoration: none; }
228 +.toc-d3 { padding-left: 14px !important; }
229 +.toc-d3 a { font-size: 12.5px !important; color: var(--muted) !important; }
230 +.hanchor { opacity: 0; margin-left: 8px; font-weight: 400; color: var(--muted); font-size: 0.85em; }
231 +h2:hover .hanchor, h3:hover .hanchor, h4:hover .hanchor { opacity: 1; text-decoration: none; }
232 +html { scroll-behavior: smooth; scroll-padding-top: 72px; }
233 +.card-head-row { display: flex; justify-content: space-between; align-items: baseline; gap: 12px; flex-wrap: wrap; }
234 +
235 +/* ---- research-log timeline ----------------------------------------------- */
236 +.timeline { margin: 24px 0; max-width: 860px; }
237 +.tl-entry { display: grid; grid-template-columns: 24px minmax(0, 1fr); gap: 14px; }
238 +.tl-rail { position: relative; }
239 +.tl-rail::before {
240 + content: ""; position: absolute; left: 11px; top: 4px; bottom: -18px; width: 2px; background: var(--grid);
241 +}
242 +.tl-entry:last-child .tl-rail::before { bottom: auto; height: 24px; }
243 +.tl-dot {
244 + position: absolute; left: 5px; top: 6px; width: 14px; height: 14px; border-radius: 50%;
245 + background: var(--accent); border: 3px solid var(--page); box-shadow: 0 0 0 1px var(--grid);
246 +}
247 +.tl-card {
248 + background: var(--surface); border: 1px solid var(--grid); border-radius: 10px;
249 + padding: 16px 20px; margin-bottom: 18px; min-width: 0;
250 +}
251 +.tl-time { font-size: 12px; color: var(--muted); font-family: ui-monospace, Menlo, monospace; }
252 +.tl-card h3 { margin: 4px 0 10px; font-size: 16.5px; letter-spacing: -0.01em; }
253 +.md-compact { font-size: 14px; color: var(--ink-2); }
254 +.md-compact ul { padding-left: 18px; margin: 6px 0; }
255 +.md-compact li { margin: 3px 0; }
256 +.md-compact p { margin: 6px 0; }
257 +.md-compact strong { color: var(--ink); }
258 +.md-compact pre { font-size: 12px; padding: 10px 12px; }
259 +
260 +/* ---- smartphone adaptation ------------------------------------------------ */
261 +@media (max-width: 720px) {
262 + body { font-size: 15px; }
263 + .wrap { padding: 0 16px; }
264 + .header-row { height: auto; flex-direction: column; align-items: flex-start; gap: 2px; padding: 10px 0; }
265 + .site-nav { width: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; flex-wrap: nowrap; padding-bottom: 4px; }
266 + .site-nav a { white-space: nowrap; padding: 6px 10px; font-size: 13.5px; }
267 + .site-header { position: static; }
268 + html { scroll-padding-top: 12px; }
269 + .hero { padding: 28px 0 4px; }
270 + .hero h1 { font-size: 26px; }
271 + .lede { font-size: 16px; }
272 + .lede-eq code { font-size: 11px; white-space: normal; word-break: break-word; }
273 + .tiles { grid-template-columns: repeat(2, 1fr); gap: 10px; margin: 20px 0; }
274 + .tile { padding: 14px 16px; }
275 + .tile-value { font-size: 26px; }
276 + .card { padding: 16px 16px; }
277 + .page-title { font-size: 24px; margin-top: 24px; }
278 + .doc-head h1 { font-size: 22px; }
279 + .doc-layout.has-toc { display: block; }
280 + .toc { position: static; max-height: none; margin-bottom: 16px; }
281 + .tl-entry { grid-template-columns: 18px minmax(0, 1fr); gap: 10px; }
282 + .tl-card { padding: 12px 14px; }
283 + .md h1 { font-size: 22px; } .md h2 { font-size: 18px; } .md h3 { font-size: 16px; }
284 + .md pre, .codeblock { font-size: 12px; padding: 10px 12px; }
285 + .results-table, .kv-table { font-size: 13px; display: block; overflow-x: auto; }
286 + .kv-table th { width: auto; }
287 + .file-head { flex-direction: column; gap: 2px; }
288 + .site-footer .wrap { flex-direction: column; }
289 + .chart-fig svg { min-width: 560px; }
290 + .chart-fig { overflow-x: auto; -webkit-overflow-scrolling: touch; }
291 +}
292 +@media (max-width: 420px) {
293 + .tiles { grid-template-columns: 1fr 1fr; }
294 + .hero h1 { font-size: 23px; }
295 +}
modified web/server.js +34 −14
@@ -56,11 +56,14 @@ app.get("/", (req, res) => {
56 56 )
57 57 .join("");
58 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 )
59 + .map((e) => {
60 + const [when, ...rest] = e.title.split(" — ");
61 + const excerpt = R.markdownToHtml(e.body.split("\n").slice(0, 6).join("\n"), "research/LOG.md").html;
62 + return `<article class="log-entry">
63 + <time class="tl-time">${R.esc(rest.length ? when : "")}</time>
64 + <h3>${R.esc(rest.join(" — ") || when)}</h3>
65 + <div class="md md-compact">${excerpt}</div></article>`;
66 + })
64 67 .join("");
65 68
66 69 const body = `
@@ -99,7 +102,8 @@ app.get("/", (req, res) => {
99 102 </section>
100 103
101 104 <section class="card">
102 <h2>Latest from the research log</h2>
105 + <div class="card-head-row"><h2>Latest from the research log</h2>
106 + <a class="more" href="/doc/research/LOG.md">Full research log →</a></div>
103 107 <div class="log-grid">${logHtml}</div>
104 108 </section>`;
105 109 page(res, { title: "Home", active: "Home", body });
@@ -126,9 +130,11 @@ app.get("/research", (req, res) => {
126 130 .filter((f) => f.name.endsWith(".md") && f.name !== "README.md")
127 131 .map((f) => {
128 132 const md = C.readMarkdown(f.rel);
129 const doc = md && md.data.document ? md.data.document : f.name;
133 + const sources = md ? (md.content.match(/^- .*http/gm) || []).length : 0;
134 + const lines = md ? md.content.split("\n").length : 0;
130 135 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>`;
136 + <p>Phase 1 literature notes — ${lines} lines.</p>
137 + <span class="badge badge-done">${sources} sources</span></a>`;
132 138 })
133 139 .join("");
134 140 const body = `<h1 class="page-title">Research documents</h1>
@@ -144,11 +150,25 @@ app.get(/^\/doc\/(.+)$/, (req, res) => {
144 150 const rel = req.params[0];
145 151 const md = rel.endsWith(".md") ? C.readMarkdown(rel) : null;
146 152 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>`;
153 + const crumb = `<p class="crumb"><a href="/research">Research</a> / ${R.esc(rel)}</p>`;
154 +
155 + if (rel === "research/LOG.md") {
156 + const entries = C.parseLogEntries();
157 + const body = `${crumb}
158 + ${R.docHeader({ ...md.data, document: "Research log" }, rel)}
159 + <p class="lede-small">Append-only, newest first — every question, experiment, result,
160 + interpretation, and decision, as required by the charter (§12). ${entries.length} entries.</p>
161 + ${R.logTimeline(entries, rel)}`;
162 + return page(res, { title: "Research log", active: "Research", body });
163 + }
164 +
165 + const { html, toc } = R.markdownToHtml(md.content, rel);
166 + const tocBox = R.tocHtml(toc);
167 + const body = `${crumb}${R.docHeader(md.data, rel)}
168 + <div class="doc-layout ${tocBox ? "has-toc" : ""}">
169 + ${tocBox}
170 + <article class="doc"><div class="md">${html}</div></article>
171 + </div>`;
152 172 page(res, { title: md.data.document || rel, active: "Research", body });
153 173 });
154 174
@@ -179,7 +199,7 @@ app.get("/experiments/:id", (req, res) => {
179 199 const md = C.readMarkdown(path.posix.join(exp.rel, file));
180 200 if (md && md.content.trim().length > 40) {
181 201 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>`);
202 + <div class="md">${R.markdownToHtml(md.content, path.posix.join(exp.rel, file)).html}</div></section>`);
183 203 }
184 204 }
185 205 const runs = C.listResultRuns().filter((r) => r.experiment === exp.id);
186 206