My aggregated monorepo of OCaml code, automaintained
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add implementation plan for 5 backlog tasks

Detailed step-by-step plan for:
- Preloaded list → Symtable runtime check (Branch A)
- jtw opam META relativization fix (Branch A)
- Missing leaflet in demo universe (Branch A)
- odoc --config flag + scrollycode theming (Branch B)
- SPA inline script execution (Branch B)
- Oxcaml investigation (no branch)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

+679
+679
docs/plans/2026-03-02-tasks-plan.md
··· 1 + # Backlog Tasks Implementation Plan 2 + 3 + > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. 4 + 5 + **Goal:** Fix five backlog issues: preloaded package detection, scrollycode theming, SPA inline scripts, jtw opam META bug, and missing leaflet in the demo universe. 6 + 7 + **Architecture:** Two independent feature branches. Branch A (`js_top_worker-fixes`) is purely in `js_top_worker`. Branch B (`scrollycode-and-spa`) touches `odoc`, `odoc-docsite`, and `odoc-scrollycode-extension`. Task 4 (oxcaml investigation) is read-only and needs no branch. 8 + 9 + **Tech Stack:** OCaml 5.4.1, dune 3.18, js_of_ocaml, Symtable/compiler-libs.bytecomp, Tyxml, Cmdliner, JavaScript (vanilla SPA shell). 10 + 11 + --- 12 + 13 + ## Setup 14 + 15 + **Step 1: Create the two feature worktrees** 16 + 17 + ```bash 18 + cd /home/jons-agent/workspace/mono 19 + monopam feature add js_top_worker-fixes 20 + monopam feature add scrollycode-and-spa 21 + ``` 22 + 23 + Work on Branch A in `../work/js_top_worker-fixes/` and Branch B in `../work/scrollycode-and-spa/`. 24 + 25 + --- 26 + 27 + ## Branch A — `js_top_worker-fixes` 28 + 29 + ### Task A1: Preloaded list → Symtable runtime check 30 + 31 + **Problem:** `js_top_worker/lib/findlibish.ml` lines 30–92 contain a hand-maintained `preloaded` list of ~50 package names that must be kept in sync with `example/dune`'s library deps. We replace it with a runtime check using OCaml's `Symtable` module. 32 + 33 + **Key facts:** 34 + - `Symtable.Global.of_ident : Ident.t -> Symtable.Global.t option` converts a persistent identifier 35 + - `Symtable.is_global_defined : Symtable.Global.t -> bool` checks if the global is in the bytecode global table (i.e. linked into the binary) 36 + - `dcs.dcs_toplevel_modules` is already the capitalised module name list for a package (e.g. `["Yojson"]`), fetched from `dynamic_cmis.json` 37 + - `compiler-libs.bytecomp` is available transitively via `compiler-libs.toplevel` (already in deps) 38 + 39 + **Files:** 40 + - Modify: `js_top_worker/lib/findlibish.ml` 41 + 42 + **Step 1: Verify the Symtable API is accessible** 43 + 44 + ```bash 45 + cd /home/jons-agent/workspace/work/js_top_worker-fixes 46 + opam exec -- ocaml -e " 47 + let id = Ident.create_persistent \"Yojson\" in 48 + match Symtable.Global.of_ident id with 49 + | None -> print_endline \"None\" 50 + | Some g -> Printf.printf \"defined: %b\n\" (Symtable.is_global_defined g) 51 + " 52 + ``` 53 + 54 + Expected: no error (just output). If it fails with a missing module error, add `compiler-libs.bytecomp` explicitly to the `js_top_worker` library's `(libraries ...)` in `js_top_worker/lib/dune`. 55 + 56 + **Step 2: Add the runtime check functions above the `preloaded` list** 57 + 58 + In `js_top_worker/lib/findlibish.ml`, add immediately before `let preloaded =`: 59 + 60 + ```ocaml 61 + let is_module_available module_name = 62 + let id = Ident.create_persistent module_name in 63 + match Symtable.Global.of_ident id with 64 + | None -> false 65 + | Some g -> Symtable.is_global_defined g 66 + 67 + let is_package_preloaded (dcs : Js_top_worker.Impl.dynamic_cmis) = 68 + List.exists is_module_available dcs.dcs_toplevel_modules 69 + ``` 70 + 71 + **Step 3: Replace the check in `require`** 72 + 73 + In `findlibish.ml` line 369, change: 74 + ```ocaml 75 + let should_load = 76 + (not (List.mem lib.name preloaded)) && not cmi_only 77 + ``` 78 + to: 79 + ```ocaml 80 + let should_load = 81 + (not (is_package_preloaded dcs)) && not cmi_only 82 + ``` 83 + 84 + **Step 4: Delete the `preloaded` list** 85 + 86 + Delete lines 20–92 (the comment and the entire `let preloaded = [...]` block). 87 + 88 + **Step 5: Build** 89 + 90 + ```bash 91 + opam exec -- dune build js_top_worker 92 + ``` 93 + 94 + Expected: clean build. If there are missing module errors for `Symtable` or `Ident`, add `compiler-libs.bytecomp` to `(libraries ...)` in `js_top_worker/lib/dune`. 95 + 96 + **Step 6: Smoke test — serve the example** 97 + 98 + ```bash 99 + opam exec -- dune build js_top_worker/example 100 + cd _build/default/js_top_worker/example && python3 -m http.server 8080 101 + ``` 102 + 103 + Open http://localhost:8080/ and run `#require "yojson";;` in the REPL. Verify it loads without "file already exists" errors in the browser console. 104 + 105 + **Step 7: Commit** 106 + 107 + ```bash 108 + git add js_top_worker/lib/findlibish.ml js_top_worker/lib/dune 109 + git commit -m "js_top_worker: replace static preloaded list with Symtable runtime check 110 + 111 + Use Symtable.Global.of_ident / is_global_defined to detect packages 112 + already linked into the worker binary at runtime, eliminating the 113 + hand-maintained list that drifts out of sync with example/dune deps. 114 + 115 + Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>" 116 + ``` 117 + 118 + --- 119 + 120 + ### Task A2: Fix META file relativization in `jtw opam` 121 + 122 + **Problem:** `js_top_worker/bin/jtw.ml` lines 206–215 use `Fpath.relativize ~root:findlib_dir meta_file |> Option.get |> Fpath.parent`. When a library is installed under `_build/install` (outside the findlib tree), `relativize` produces a path with `..` segments, and the `Option.get` call panics with `None` in edge cases. 123 + 124 + **Fix:** Use the existing `relativize_or_fallback` helper (lines 11–29), already used for all CMI file paths. 125 + 126 + **Files:** 127 + - Modify: `js_top_worker/bin/jtw.ml:206-215` 128 + 129 + **Step 1: Apply the fix** 130 + 131 + Change lines 206–215 from: 132 + ```ocaml 133 + let meta_rels = 134 + Util.StringSet.fold 135 + (fun meta_file acc -> 136 + let meta_file = Fpath.v meta_file in 137 + let d = 138 + Fpath.relativize ~root:findlib_dir meta_file 139 + |> Option.get |> Fpath.parent 140 + in 141 + (meta_file, d) :: acc) 142 + meta_files [] 143 + in 144 + ``` 145 + to: 146 + ```ocaml 147 + let meta_rels = 148 + Util.StringSet.fold 149 + (fun meta_file acc -> 150 + let meta_file = Fpath.v meta_file in 151 + let d = relativize_or_fallback ~findlib_dir meta_file |> Fpath.parent in 152 + (meta_file, d) :: acc) 153 + meta_files [] 154 + in 155 + ``` 156 + 157 + **Step 2: Build** 158 + 159 + ```bash 160 + opam exec -- dune build js_top_worker/bin 161 + ``` 162 + 163 + Expected: clean build. 164 + 165 + **Step 3: Smoke test** 166 + 167 + ```bash 168 + opam exec -- jtw opam -o /tmp/test-universe yojson 169 + ls /tmp/test-universe/lib/ 170 + ``` 171 + 172 + Expected: META files appear, no `Invalid_argument` or `None` exception. 173 + 174 + **Step 4: Commit** 175 + 176 + ```bash 177 + git add js_top_worker/bin/jtw.ml 178 + git commit -m "jtw opam: fix META file relativization for _build/install paths 179 + 180 + Use relativize_or_fallback for META files (already used for CMI files), 181 + fixing a None exception when libraries are installed outside the 182 + standard findlib tree. 183 + 184 + Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>" 185 + ``` 186 + 187 + --- 188 + 189 + ### Task A3: Add `js_top_worker-widget-leaflet` to demo universe 190 + 191 + **Problem:** `odoc-interactive-extension/doc/dune` only lists `yojson` in the `jtw opam` universe build. The map demo requires `js_top_worker-widget-leaflet`, which is absent from the universe so `#require "js_top_worker-widget-leaflet"` in the demo fails. 192 + 193 + **Files:** 194 + - Modify: `odoc-interactive-extension/doc/dune:16` 195 + 196 + **Step 1: Apply the fix** 197 + 198 + Change line 16 from: 199 + ``` 200 + (run jtw opam -o universe yojson))) 201 + ``` 202 + to: 203 + ``` 204 + (run jtw opam -o universe yojson js_top_worker-widget-leaflet))) 205 + ``` 206 + 207 + **Step 2: Build the universe** 208 + 209 + ```bash 210 + opam exec -- dune build odoc-interactive-extension/doc/universe 211 + ls _build/default/odoc-interactive-extension/doc/universe/lib/ | grep leaflet 212 + ``` 213 + 214 + Expected: leaflet-related `.cma.js` and `.cmi` files appear. 215 + 216 + **Step 3: Commit** 217 + 218 + ```bash 219 + git add odoc-interactive-extension/doc/dune 220 + git commit -m "odoc-interactive-extension: add js_top_worker-widget-leaflet to universe 221 + 222 + The map demo requires this widget package at runtime. 223 + 224 + Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>" 225 + ``` 226 + 227 + --- 228 + 229 + ## Branch B — `scrollycode-and-spa` 230 + 231 + ### Task B1: Add `config_values` to `Html.Config.t` 232 + 233 + **Problem:** We need a way to pass arbitrary `key=value` pairs from `odoc html-generate`'s CLI through to shells (and potentially extensions). The existing `Config.t` record is the right place. 234 + 235 + **Files:** 236 + - Modify: `odoc/src/html/config.ml` 237 + - Modify: `odoc/src/html/config.mli` 238 + 239 + **Step 1: Add field to `config.ml`** 240 + 241 + In `odoc/src/html/config.ml`, change the type definition to add after `home_breadcrumb`: 242 + ```ocaml 243 + config_values : (string * string) list; 244 + ``` 245 + 246 + Change `v` to accept the new parameter (add before `unit ->`): 247 + ```ocaml 248 + ?(config_values = []) -> 249 + ``` 250 + 251 + Add to the record literal in `v`: 252 + ```ocaml 253 + config_values; 254 + ``` 255 + 256 + Add the accessor at the bottom: 257 + ```ocaml 258 + let config_values config = config.config_values 259 + ``` 260 + 261 + **Step 2: Update `config.mli`** 262 + 263 + Add `?config_values:(string * string) list ->` to the `v` signature (before `unit ->`). 264 + 265 + Add the accessor: 266 + ```ocaml 267 + val config_values : t -> (string * string) list 268 + ``` 269 + 270 + **Step 3: Build** 271 + 272 + ```bash 273 + opam exec -- dune build odoc/src/html 274 + ``` 275 + 276 + Expected: clean build. Any callers of `Config.v` that don't pass `config_values` will still work (it defaults to `[]`). 277 + 278 + **Step 4: Commit** 279 + 280 + ```bash 281 + git add odoc/src/html/config.ml odoc/src/html/config.mli 282 + git commit -m "odoc html: add config_values field to Html.Config.t 283 + 284 + Allows passing arbitrary key=value configuration from the CLI 285 + through to shell plugins and extensions. 286 + 287 + Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>" 288 + ``` 289 + 290 + --- 291 + 292 + ### Task B2: Add `--config` CLI argument to `odoc html-generate` 293 + 294 + **Files:** 295 + - Modify: `odoc/src/odoc/bin/main.ml` 296 + 297 + The relevant section is `Odoc_html_args.extra_args` (around line 1313). We need to add a `--config key=value` repeatable arg, parse the `=`, and pass the result to `Config.v`. 298 + 299 + **Step 1: Add the arg term** 300 + 301 + In `Odoc_html_args` module, add a new `let config_values_raw` term (alongside the other `let` bindings for args, before `let extra_args`): 302 + 303 + ```ocaml 304 + let config_values_raw = 305 + let doc = 306 + "A configuration key=value pair passed to the shell plugin. \ 307 + May be repeated, e.g. $(b,--config scrollycode.theme=warm)." 308 + in 309 + Arg.(value & opt_all string [] & info [ "config" ] ~docv:"KEY=VALUE" ~doc) 310 + in 311 + ``` 312 + 313 + **Step 2: Wire it into `extra_args`** 314 + 315 + Change the `config` function signature inside `extra_args` to add `config_values_raw` as a final parameter, then: 316 + 317 + ```ocaml 318 + let config_values = 319 + List.filter_map (fun s -> 320 + match String.index_opt s '=' with 321 + | None -> None 322 + | Some i -> 323 + let k = String.sub s 0 i in 324 + let v = String.sub s (i + 1) (String.length s - i - 1) in 325 + Some (k, v)) 326 + config_values_raw 327 + in 328 + let html_config = 329 + Odoc_html.Config.v ~theme_uri ~support_uri ~search_uris ~extra_css 330 + ~semantic_uris ~indent ~flat ~open_details ~as_json ?shell ~remap 331 + ?home_breadcrumb ~config_values () 332 + in 333 + ``` 334 + 335 + Add `$ config_values_raw` to the `Term.(const config $ ...)` application at the end of `extra_args`. 336 + 337 + **Step 3: Build** 338 + 339 + ```bash 340 + opam exec -- dune build odoc/src/odoc 341 + ``` 342 + 343 + Expected: clean build. 344 + 345 + **Step 4: Verify the flag appears** 346 + 347 + ```bash 348 + opam exec -- dune exec odoc -- html-generate --help | grep config 349 + ``` 350 + 351 + Expected: `--config KEY=VALUE` appears in the help output. 352 + 353 + **Step 5: Commit** 354 + 355 + ```bash 356 + git add odoc/src/odoc/bin/main.ml 357 + git commit -m "odoc html-generate: add --config KEY=VALUE argument 358 + 359 + Passes arbitrary key=value pairs into Html.Config.t for use by 360 + shell plugins. Example: --config scrollycode.theme=warm 361 + 362 + Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>" 363 + ``` 364 + 365 + --- 366 + 367 + ### Task B3: odoc-docsite shell reads `scrollycode.theme` from config 368 + 369 + **Files:** 370 + - Modify: `odoc-docsite/src/odoc_docsite_shell.ml` 371 + 372 + The `page_creator` function (line 112) assembles the `<head>`. We add a helper that, if `scrollycode.theme` is present in `Config.config_values`, emits a `<link>` for the corresponding theme CSS. The same helper is used by `src_page_creator`. 373 + 374 + **Step 1: Add the helper** 375 + 376 + Add a new function after the `file_uri` helper (around line 29): 377 + 378 + ```ocaml 379 + let scrollycode_theme_links ~config ~url = 380 + match 381 + List.assoc_opt "scrollycode.theme" 382 + (Odoc_html.Config.config_values config) 383 + with 384 + | None -> [] 385 + | Some theme -> 386 + let support_uri = Odoc_html.Config.support_uri config in 387 + let css_url = 388 + file_uri ~config ~url support_uri 389 + ("extensions/scrollycode-" ^ theme ^ ".css") 390 + in 391 + [ Html.link ~rel:[ `Stylesheet ] ~href:css_url () ] 392 + ``` 393 + 394 + **Step 2: Include in `page_creator` head** 395 + 396 + In `page_creator`, change line 226: 397 + ```ocaml 398 + @ katex_elements @ extension_head_elements 399 + ``` 400 + to: 401 + ```ocaml 402 + @ katex_elements @ extension_head_elements 403 + @ scrollycode_theme_links ~config ~url 404 + ``` 405 + 406 + **Step 3: Include in `src_page_creator` head** 407 + 408 + Find the equivalent head-assembly line in `src_page_creator` (around line 370–390) and add `@ scrollycode_theme_links ~config ~url` there too. 409 + 410 + **Step 4: Build** 411 + 412 + ```bash 413 + opam exec -- dune build odoc-docsite 414 + ``` 415 + 416 + Expected: clean build. 417 + 418 + **Step 5: Verify with a quick test** 419 + 420 + ```bash 421 + opam exec -- dune build @doc # build any package docs 422 + # Then check that pages without --config don't have the theme link 423 + grep -r "scrollycode-warm" _build/default/_doc/_html/ 2>/dev/null | wc -l 424 + ``` 425 + 426 + Expected: 0 lines (no theme link without config). 427 + 428 + **Step 6: Commit** 429 + 430 + ```bash 431 + git add odoc-docsite/src/odoc_docsite_shell.ml 432 + git commit -m "odoc-docsite: include scrollycode theme CSS when scrollycode.theme config is set 433 + 434 + Reads 'scrollycode.theme' from Html.Config.config_values and emits 435 + a <link> for the corresponding scrollycode-{theme}.css support file. 436 + 437 + Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>" 438 + ``` 439 + 440 + --- 441 + 442 + ### Task B4: Apply scrollycode theme to the demos 443 + 444 + The scrollycode demo docs are currently built without a theme. We need to pass `--config scrollycode.theme=warm` to `odoc html-generate` for those pages. 445 + 446 + **Files:** 447 + - Investigate: `odoc-scrollycode-extension/` — check if dune's `(documentation ...)` stanza supports `(generate_flags ...)` in dune 3.18 448 + 449 + **Step 1: Check dune support for generate flags** 450 + 451 + ```bash 452 + grep -r "generate_flags" $(opam exec -- ocamlfind query -format "%d" dune-configurator 2>/dev/null)/../.. 2>/dev/null | head -5 453 + # OR just try it: 454 + ``` 455 + 456 + In `odoc-scrollycode-extension/doc/dune` (or wherever the `(documentation ...)` stanza is for the demos), try adding: 457 + 458 + ``` 459 + (documentation 460 + (package odoc-scrollycode-extension) 461 + (flags (:standard --config scrollycode.theme=warm))) 462 + ``` 463 + 464 + Build and verify. If dune doesn't support `(flags ...)` for `(documentation ...)`, fall back to adding a custom dune rule that calls `odoc html-generate` explicitly with `--config scrollycode.theme=warm`. 465 + 466 + **Step 2: Build with theme** 467 + 468 + ```bash 469 + opam exec -- dune build @doc-private 470 + # OR: 471 + opam exec -- dune build odoc-scrollycode-extension 472 + ``` 473 + 474 + **Step 3: Verify** 475 + 476 + ```bash 477 + grep "scrollycode-warm" _build/default/_doc/_html/odoc-scrollycode-extension/*.html | head -5 478 + ``` 479 + 480 + Expected: theme CSS link appears in the generated HTML. 481 + 482 + **Step 4: Commit** 483 + 484 + ```bash 485 + git add odoc-scrollycode-extension/ 486 + git commit -m "odoc-scrollycode-extension: apply warm theme to demos via --config 487 + 488 + Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>" 489 + ``` 490 + 491 + --- 492 + 493 + ### Task B5: Fix SPA shell to execute inline scripts on navigation 494 + 495 + **Problem:** `navigateTo` in `odoc_docsite_js.ml` (lines 315–335) injects `link[rel="stylesheet"]` and `script[src]` from a fetched page but ignores `script` elements without a `src` attribute. Extensions like mermaid, msc, dot, and scrollycode use `Js_inline` for init scripts that must run on every navigation. 496 + 497 + **Fix:** After injecting external scripts (tracking their load events), execute all inline scripts from the fetched page's head once external scripts have loaded. 498 + 499 + **Files:** 500 + - Modify: `odoc-docsite/src/odoc_docsite_js.ml` (the JavaScript source embedded as an OCaml string) 501 + 502 + **Step 1: Find the JS source in `odoc_docsite_js.ml`** 503 + 504 + The file is auto-generated or contains a literal JS string. Read it: 505 + ```bash 506 + head -30 odoc-docsite/src/odoc_docsite_js.ml 507 + ``` 508 + 509 + The JS lives either directly in that file as a string literal or is compiled from a `.js` file. Find the `navigateTo` function. 510 + 511 + **Step 2: Update the script injection in `navigateTo`** 512 + 513 + The current block (around lines 315–335 of the *JavaScript*, not the OCaml file): 514 + 515 + ```javascript 516 + doc.querySelectorAll('head link[rel="stylesheet"], head script[src]').forEach(function(el) { 517 + var attr = el.tagName === 'LINK' ? 'href' : 'src'; 518 + var resUrl = el.getAttribute(attr); 519 + if (!resUrl) return; 520 + var abs = new URL(resUrl, fetchedPageBase).href; 521 + var selector = el.tagName === 'LINK' 522 + ? 'link[rel="stylesheet"]' 523 + : 'script[src]'; 524 + var already = Array.from(document.querySelectorAll('head ' + selector)).some(function(existing) { 525 + var existingUrl = existing.getAttribute(attr); 526 + if (!existingUrl) return false; 527 + return new URL(existingUrl, window.location.href).href === abs; 528 + }); 529 + if (!already) { 530 + var clone = el.cloneNode(true); 531 + // Fix relative URL to be root-relative 532 + clone.setAttribute(attr, abs); 533 + document.head.appendChild(clone); 534 + } 535 + }); 536 + ``` 537 + 538 + Replace with: 539 + 540 + ```javascript 541 + // Collect inline scripts first (before we start loading external scripts) 542 + var inlineScripts = Array.from(doc.querySelectorAll('head script:not([src])')); 543 + 544 + // Inject external CSS/JS; track load events for newly added scripts 545 + var newScriptLoadPromises = []; 546 + doc.querySelectorAll('head link[rel="stylesheet"], head script[src]').forEach(function(el) { 547 + var attr = el.tagName === 'LINK' ? 'href' : 'src'; 548 + var resUrl = el.getAttribute(attr); 549 + if (!resUrl) return; 550 + var abs = new URL(resUrl, fetchedPageBase).href; 551 + var selector = el.tagName === 'LINK' 552 + ? 'link[rel="stylesheet"]' 553 + : 'script[src]'; 554 + var already = Array.from(document.querySelectorAll('head ' + selector)).some(function(existing) { 555 + var existingUrl = existing.getAttribute(attr); 556 + if (!existingUrl) return false; 557 + return new URL(existingUrl, window.location.href).href === abs; 558 + }); 559 + if (!already) { 560 + var clone = el.cloneNode(true); 561 + clone.setAttribute(attr, abs); 562 + if (el.tagName === 'SCRIPT') { 563 + var p = new Promise(function(resolve) { 564 + clone.onload = resolve; 565 + clone.onerror = resolve; // don't block the chain on error 566 + }); 567 + newScriptLoadPromises.push(p); 568 + } 569 + document.head.appendChild(clone); 570 + } 571 + }); 572 + 573 + // After all newly added external scripts have loaded, execute inline scripts 574 + Promise.all(newScriptLoadPromises).then(function() { 575 + inlineScripts.forEach(function(el) { 576 + var s = document.createElement('script'); 577 + s.textContent = el.textContent; 578 + document.head.appendChild(s); 579 + }); 580 + }); 581 + ``` 582 + 583 + **Step 3: Build** 584 + 585 + ```bash 586 + opam exec -- dune build odoc-docsite 587 + ``` 588 + 589 + Expected: clean build. 590 + 591 + **Step 4: Manual test — mermaid** 592 + 593 + - Build the mermaid extension docs or any page with `@mermaid` content 594 + - Serve them 595 + - Navigate from a non-mermaid page to a mermaid page via the SPA 596 + - Verify the diagram renders (not just raw text) 597 + 598 + **Step 5: Commit** 599 + 600 + ```bash 601 + git add odoc-docsite/src/odoc_docsite_js.ml 602 + git commit -m "odoc-docsite: execute extension inline scripts on SPA navigation 603 + 604 + navigateTo now collects head script:not([src]) elements from the 605 + fetched page and executes them after any newly added external scripts 606 + have loaded. This fixes mermaid, msc, dot, and scrollycode meta-tag 607 + initialisation on SPA navigation. 608 + 609 + Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>" 610 + ``` 611 + 612 + --- 613 + 614 + ### Task B6: Final build and smoke test 615 + 616 + ```bash 617 + opam exec -- dune build 618 + opam exec -- dune test 619 + ``` 620 + 621 + Expected: full clean build and all tests pass. 622 + 623 + --- 624 + 625 + ## Task 4 — Oxcaml modes/layouts investigation (no branch) 626 + 627 + **Goal:** Determine whether the discrepancy between odoc and utop output for OxCaml mode/layout annotations is a bug or expected behaviour. 628 + 629 + **Example discrepancy:** 630 + 631 + - odoc renders: `val compare__local : 'a. (('a : value_or_null) @ local -> ...) @ local -> ...` 632 + - utop shows: `val compare__local : ('a @ local -> 'a @ local -> int) -> 'a Base.Uniform_array.t @ local -> 'a Base.Uniform_array.t @ local -> int` 633 + 634 + **Step 1: Read the type definition** 635 + 636 + ```bash 637 + grep -n "compare__local" /cache/jons-agent/oxmono/lib/base/src/uniform_array.mli 2>/dev/null | head -10 638 + # Or find the right file: 639 + find /cache/jons-agent/oxmono -name "uniform_array*" | head -5 640 + ``` 641 + 642 + **Step 2: Check what odoc generates for the type** 643 + 644 + Build odoc output for `base` in oxmono: 645 + 646 + ```bash 647 + cd /cache/jons-agent/oxmono 648 + opam exec -- dune build @doc 2>&1 | head -50 649 + # Find the generated HTML for Base.Uniform_array 650 + grep -r "compare__local" _build/default/_doc/_html/base/ 2>/dev/null | head -5 651 + ``` 652 + 653 + **Step 3: Compare odoc's internal representation** 654 + 655 + Check the `.odoc` file for the function signature: 656 + 657 + ```bash 658 + opam exec -- odoc inspect <path-to-base.odoc> 2>/dev/null | grep -A5 "compare__local" 659 + ``` 660 + 661 + **Step 4: Document findings** 662 + 663 + Write a brief report covering: 664 + - What the source `.mli` says 665 + - What odoc generates 666 + - What utop shows 667 + - Whether the difference is in rendering (parenthesisation, explicit vs implicit modes) or in semantics 668 + - Recommendation: bug to file, or expected/acceptable difference 669 + 670 + Save to `docs/plans/2026-03-02-oxcaml-investigation.md`. 671 + 672 + --- 673 + 674 + ## Completion checklist 675 + 676 + - [ ] Branch A merged: preloaded list removed, jtw META fixed, leaflet in universe 677 + - [ ] Branch B merged: `--config` added to odoc, docsite applies scrollycode theme, SPA executes inline scripts 678 + - [ ] Task 4 investigation report written 679 + - [ ] Both `monopam feature` worktrees synced back via `monopam sync --remote`