diff --git a/.claude/settings.json b/.claude/settings.json index a55838b..d7b3b66 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -51,5 +51,8 @@ ] } ] + }, + "enabledPlugins": { + "mattpocock-skills@claude-plugins-official": true } } diff --git a/.claude/skills/README.md b/.claude/skills/README.md index 3135f8c..fd9d15a 100644 --- a/.claude/skills/README.md +++ b/.claude/skills/README.md @@ -43,3 +43,12 @@ you changed a skill on purpose, run `scripts/readme.sh --lock` and commit the lo | after an edit of `README.md`, and before the session ends | `.claude/settings.json` → `scripts/readme.sh --hook` | tells the agent to use `easy-pt-br` while the easy version is older than `README.md` | | before a commit | `.githooks/pre-commit` (turn on with `just hooks`) | runs `scripts/readme.sh --check` when a README or a skill is staged | | in CI | `just check` | the same check, for every pull request | + +## Plugins + +`.claude/settings.json` turns on these Claude Code plugins for everyone who opens this +repository. Claude Code asks each reader to trust and install them once. + +| Plugin | From | +|---|---| +| `mattpocock-skills` | the `claude-plugins-official` marketplace | diff --git a/.claude/skills/skills.lock b/.claude/skills/skills.lock index ba0f85d..b4211d7 100644 --- a/.claude/skills/skills.lock +++ b/.claude/skills/skills.lock @@ -2,7 +2,7 @@ "version": 1, "note": "every skill here was written for this repository; none is a copy. SOURCES in each SKILL.md and in README.md say what each one read. sha256 of each file, checked by scripts/readme.sh --check.", "files": { - "README.md": "d6cc5605db95be46fd2e1b077739cdd8e5d897573ee755c03399c281dbe8fceb", + "README.md": "d776fed8202f51b478effe71862d1ee1f10b9949c4789af02def06c1c614d83b", "card-style/SKILL.md": "42755313375b690efb8f47fd3fd73f29eba21e07c04612c980cd2abbdfd70941", "citation-score/SKILL.md": "7f58cb381bc9dc0d0a6536e5b56f3bd826b61c057c80ada2f95349a0abba0a24", "easy-pt-br/SKILL.md": "5d7d5383721c3cd38694a425d9b449f8f70ad064ffd4179636bcbeb24eada7ba", diff --git a/.gitignore b/.gitignore index 84258b2..fef7be3 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ graphify-out/ # The Unison codebase of `just unison-install` (unison/refpulse/README.md) unison/.codebase/ unison/refpulse/install.output.md +.unisonHistory diff --git a/site/src/closed_source_site/app.gleam b/site/src/closed_source_site/app.gleam index 85a9802..af20c44 100644 --- a/site/src/closed_source_site/app.gleam +++ b/site/src/closed_source_site/app.gleam @@ -713,10 +713,11 @@ fn timeline() -> Element(Msg) { } } -/// The refpulse service's URL on Unison Cloud, printed by `just unison-deploy`. Empty until it is -/// deployed: then the button says so, unless the page is opened with `?refpulse=` (a local -/// `just unison-serve`, for example). -pub const refpulse_url = "" +/// The refpulse service's URL on Unison Cloud, printed by `just unison-deploy`. A page opened with +/// `?refpulse=` asks that URL instead (a local `just unison-serve`, for example). +/// shortcut: the hash URL, because naming the service failed (401); use .../s/refpulse once it is named. +pub const refpulse_url = + "https://h0ffmann.unison-services.cloud/h/uidn3rn5oag2ww53c5h4tltjlje76o3nlm44ct7tyjarcn24j2eq" /// The book's own DOIs: the concept DOI, then each version's. pub fn book_dois() -> List(String) { @@ -762,6 +763,11 @@ fn live_section(model: Model) -> Element(Msg) { "This page is static, on GitHub Pages. The button asks a Unison service, refpulse, to check now that each of the book's DOIs resolves at doi.org, and shows its answer here. A live answer, not a fact of the book.", ), ]), + html.p([attribute.class("sub")], [ + html.text( + "Unison gives each piece of code a fingerprint, a hash of the code itself. So refpulse was not installed on a server: its code was sent to Unison Cloud, which runs it, and the long part of its address is that fingerprint. The click itself is an ordinary web request. Checking the DOIs on many machines at once is the next step.", + ), + ]), html.button( [ attribute.class("pulse-button"), diff --git a/unison/refpulse/README.md b/unison/refpulse/README.md index bf4be66..9ddc04f 100644 --- a/unison/refpulse/README.md +++ b/unison/refpulse/README.md @@ -19,6 +19,35 @@ browser (h0ffmann.github.io) refpulse (Unison) doi. draw ◄── 200 JSON, CORS: * ◄── {"checked","ran","results":[…]} ``` +## In plain words + +**What is Unison?** A programming language in which each piece of code has a fingerprint. The +fingerprint is a hash of the code itself: "Each Unison definition is identified by a hash of its +syntax tree" (*The big idea*). Two computers that agree on a fingerprint agree on the code, to +the last letter. So a computer can send code to another computer, and the other computer asks +only for the parts that it does not have yet. Unison's own words: "arbitrary computations can +just be moved from one location to another, with missing dependencies deployed on the fly". + +**Like you are five.** Most web services are a shop: someone builds a kitchen (a server, a +container, a build), and you phone in an order. With Unison, you post the recipe itself, and the +cloud cooks it. The recipe has a fingerprint, so nobody can mix it up with another recipe. + +**Is the button a remote computation, or a simple API call?** Both, at two different steps. + +1. **The deploy is a remote computation.** `just unison-deploy` does not upload a program to a + server that we keep. It sends the function `Route.run refpulse.routes` to Unison Cloud, and + Unison Cloud runs it. Nothing is built: no container, no virtual machine, no YAML. Unison + Cloud says: "The hash is created from the code being deployed; it uniquely identifies the + service deployment". That hash is the long part of refpulse's address, + `/h/uidn3rn5…j2eq`. The address names the code, not a machine. +2. **The click is an API call.** The page sends `GET /check?doi=…` and receives JSON, as with any + web service. The browser does not know that Unison is at the other end. + +**What is not done yet.** refpulse checks the DOIs one after another, on one machine. Unison can +also move a computation to other machines while it runs (`Remote.fork`; "the functions are +'moved to the data'", *Spark-like distributed datasets*). With that step, refpulse would check +all the DOIs at the same time, on different machines. It is the "Next" item below. + ## The contract | Request | Answer | @@ -92,14 +121,19 @@ call `localhost`, after the prompt. A tunnel's public HTTPS address does not nee in headless Chrome. The click went out, the answer came back, and the page drew "1 of 1 DOIs resolve · 302". With the service down, it showed "refpulse did not answer". With no URL, it showed "refpulse is not deployed yet". -- **Not yet checked: `refpulse.u` itself.** It was written against the names and types that - Unison Share's API gives for `@unison/cloud` 27.3.1, `@unison/routes` 7.0.2, `@unison/http` - 16.1.0, `@unison/json` 1.4.2 and `@unison/base` 7.20.0. It was not compiled, because the agent's - sandbox could not download the libraries (`lib.install` stopped with `IncompleteHeaders`). - `just unison-install` compiles it; `install.output.md` shows any error. +- **Checked (15 October 2026): `refpulse.u` itself.** `just unison-install` compiles it against + `@unison/base` 7.20.0, `@unison/cloud` 27.3.1 and `@unison/routes` 7.0.2. `just unison-serve` + ran it in the local runtime. `GET /health` gave 200. `GET /check` gave 302 for the book's DOI + and 404 for a made-up one, with `Access-Control-Allow-Origin: *`. +- **Deployed (15 October 2026).** `just unison-deploy` put refpulse on Unison Cloud at + `https://h0ffmann.unison-services.cloud/h/uidn3rn5oag2ww53c5h4tltjlje76o3nlm44ct7tyjarcn24j2eq`. + It answered `/health` 200, and `/check` 302 and 404, with the CORS header. The site's + `refpulse_url` is this address. The step after it, `ServiceName.assign` (the stable name + `/s/refpulse`), failed with 401 Unauthorized. When the name works, put `/s/refpulse` in + `refpulse_url`: a name stays the same when the code changes, and a hash does not. - **Next.** The checks run one after another, which takes about 0.5 s per DOI. `Remote.fork` and `Remote.await` could run them at the same time, on many machines, if the forked task may call - `Http`. That is the "distributed" step, after this one compiles. + `Http`. That is the "distributed" step. ## Sources @@ -110,3 +144,10 @@ call `localhost`, after the prompt. A tunnel's public HTTPS address does not nee `response.headers.add`, the CORS middleware example. - `@unison/cloud` 27.3.1 README, , read the same way: `deployHttp`, `ServiceName.assign`, `parMap`, `Remote`. +- Unison, "The big idea", , read 15 October + 2026: definitions identified by a hash; computations moved with their dependencies. +- Unison Cloud, home page, , read 15 October 2026: deployments get + "a unique content-addressed hash"; no containers, VMs or YAML. +- Unison, "Spark-like distributed datasets in under 100 lines of Unison", + , read 15 October 2026: functions + "moved to the data". diff --git a/unison/refpulse/install.md b/unison/refpulse/install.md index 814db55..711b2d1 100644 --- a/unison/refpulse/install.md +++ b/unison/refpulse/install.md @@ -8,6 +8,7 @@ A UCM transcript (`just unison-install`): it makes the `refpulse` project in the ```ucm scratch/main> project.create-empty refpulse +refpulse/main> lib.install @unison/base refpulse/main> lib.install @unison/cloud refpulse/main> lib.install @unison/routes refpulse/main> load unison/refpulse/refpulse.u diff --git a/unison/refpulse/live-check.png b/unison/refpulse/live-check.png new file mode 100644 index 0000000..982e35a Binary files /dev/null and b/unison/refpulse/live-check.png differ diff --git a/unison/refpulse/refpulse.u b/unison/refpulse/refpulse.u index c9bbd3f..bf79d9f 100644 --- a/unison/refpulse/refpulse.u +++ b/unison/refpulse/refpulse.u @@ -50,7 +50,7 @@ refpulse.routes = do , ("results", Json.array results) ]) refpulse.cors() - health <|> check + (health <|> check)() -- On this machine: the Cloud runtime, locally. It prints the service's URL and serves until Enter. refpulse.deploy.local : '{IO, Exception} ()