···11+# Design: Per-package findlib_index.json
22+33+## Problem
44+55+day10 currently produces universe-level entry points at
66+`u/<universe-hash>/findlib_index.json`. The universe hash is an opaque
77+content address computed from build hashes — an `.mld` author cannot
88+predict it, so they must either hard-code it after a build or use the
99+separate `@x-ocaml.worker` and `@x-ocaml.universe` tags with explicit
1010+hashes.
1111+1212+What we want: a stable, human-readable entry point at
1313+`p/<pkg>/<ver>/findlib_index.json` so an `.mld` file can say:
1414+1515+```
1616+@x-ocaml.universe ../../p/yojson/3.0.0
1717+```
1818+1919+and everything is discoverable from that one URL — which worker to load,
2020+where every library's META/cmi/cma.js files are, all with content hashes
2121+for immutable caching.
2222+2323+## Design
2424+2525+### Output layout (unchanged paths + one new file)
2626+2727+```
2828+compiler/<ver>/<worker-hash>/
2929+ worker.js
3030+ lib/ocaml/stdlib.cmi, stdlib.cma.js, ...
3131+3232+p/<pkg>/<ver>/<content-hash>/
3333+ lib/<findlib-name>/META, *.cmi, *.cma.js, dynamic_cmis.json
3434+3535+p/<pkg>/<ver>/findlib_index.json <-- NEW
3636+3737+u/<universe-hash>/findlib_index.json (kept for backward compat)
3838+```
3939+4040+### Per-package findlib_index.json format
4141+4242+`p/yojson/3.0.0/findlib_index.json`:
4343+4444+```json
4545+{
4646+ "compiler": {
4747+ "version": "5.4.1",
4848+ "content_hash": "1f7e4fa461714841"
4949+ },
5050+ "meta_files": [
5151+ "4c22b3794e61bc3f/lib/yojson/META",
5252+ "../../p/seq/base/85ab713df4290233/lib/seq/META",
5353+ "../../p/dune/3.21.1/5f2666121f6b317c/lib/dune/META",
5454+ "../../compiler/5.4.1/1f7e4fa461714841/lib/ocaml/stdlib/META"
5555+ ]
5656+}
5757+```
5858+5959+All paths are relative to this file's location (`p/yojson/3.0.0/`).
6060+6161+- **`compiler`** — version + content_hash. x-ocaml uses this to
6262+ construct `compiler/<ver>/<hash>/worker.js` when no explicit
6363+ `@x-ocaml.worker` override is present.
6464+- **`meta_files`** — flat list of every META path needed for this
6565+ package's complete transitive dependency set. Every path contains a
6666+ content hash, so all referenced files are immutable.
6767+- No `universes` field — the flat list means one fetch gives the client
6868+ everything. No recursive lookups needed.
6969+7070+### Why flat, not recursive
7171+7272+The `jtw opam` multiverse mode uses per-package `findlib_index.json`
7373+with a `universes` field linking to deps, requiring the client to chase
7474+the graph. For a server like ocaml.org this means N+1 fetches (one per
7575+transitive dep). A flat list means exactly one fetch to discover
7676+everything. The client (findlibish.ml) already handles flat meta_files
7777+lists — no changes needed there.
7878+7979+### Immutability and consistency
8080+8181+Every path in `meta_files` includes a content hash. If ocaml.org
8282+rebuilds a package, the content hash changes and a new
8383+`p/<pkg>/<ver>/<new-hash>/` directory appears. The findlib_index.json is
8484+then updated atomically to point at the new hashes. This is the only
8585+mutable file — everything it references is immutable and CDN-cacheable.
8686+8787+Without content hashes in the paths, a client could fetch the index,
8888+then fetch a META file that has been replaced by a rebuild, leading to
8989+inconsistent state. Content hashes eliminate this race.
9090+9191+### .mld usage
9292+9393+```
9494+{0 Yojson Tutorial}
9595+9696+@x-ocaml.universe ../../p/yojson/3.0.0
9797+9898+{@ocaml[
9999+#require "yojson"
100100+]}
101101+102102+{@ocaml[
103103+let json = `Assoc [("hello", `String "world")]
104104+let () = print_endline (Yojson.Safe.pretty_to_string json)
105105+]}
106106+```
107107+108108+One tag. x-ocaml.js fetches `../../p/yojson/3.0.0/findlib_index.json`,
109109+reads `compiler` to load the correct per-solution worker, and resolves
110110+all META paths for findlibish to consume.
111111+112112+### Changes needed
113113+114114+#### 1. day10 `assemble_jtw_output` (jtw_gen.ml)
115115+116116+After assembling p/ directories and writing u/<hash>/findlib_index.json,
117117+also write `p/<pkg>/<ver>/findlib_index.json` for each target package.
118118+The content is the compiler info + the same meta_files list already
119119+computed for the universe index, but with paths adjusted to be relative
120120+to `p/<pkg>/<ver>/` instead of `u/<hash>/`.
121121+122122+#### 2. x-ocaml.js (x_ocaml.ml)
123123+124124+Currently the worker URL comes from `@x-ocaml.worker` meta tag or
125125+`src-worker` attribute. Add: if findlib_index.json has a `compiler`
126126+field and no explicit `@x-ocaml.worker` override, construct the worker
127127+URL as `<base>/compiler/<ver>/<hash>/worker.js` where `<base>` is
128128+resolved relative to the findlib_index URL.
129129+130130+This requires x-ocaml.js to fetch the findlib_index *before* creating
131131+the worker (currently it creates the worker first and passes the
132132+findlib_index URL to it). The flow becomes:
133133+134134+1. Fetch findlib_index.json
135135+2. If it has `compiler`, construct worker URL from it
136136+3. Create worker with that URL
137137+4. Pass findlib_index URL to worker for META loading
138138+139139+#### 3. findlibish.ml — no changes
140140+141141+Already handles flat `meta_files` lists. The `universes` field is
142142+optional and simply won't be present.
143143+144144+#### 4. u/<hash>/ directories — kept
145145+146146+Continue generating them for backward compatibility with existing
147147+deployments. No changes to existing format.
148148+149149+## Non-goals
150150+151151+- Changing the `jtw opam` multiverse format (that's a different use case
152152+ for local development)
153153+- Removing the u/<hash>/ universe directories
154154+- Changing how per-package p/<pkg>/<ver>/<hash>/ artifacts are generated