prolog-notebook 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,238 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.0] — 2026-08-30
4
+
5
+ **A chapter is a file you can read, run, and fill in from the command line.** The renderer,
6
+ the page's own behaviour, and the first half of the CLI.
7
+
8
+ ### Why
9
+
10
+ 0.2.0 made a `.prolog.md` executable. It did not make one *publishable*: the answers a chapter
11
+ shows had to be typed by its author, which means they were the author's guess at what SWI
12
+ prints, published as though it ran. Everything here follows from closing that gap and from
13
+ what the closing revealed.
14
+
15
+ `prolog-notebook run` now fills a chapter's answers in from a real engine. Run against the
16
+ chapter in this repository it changes nothing — the hand-written answers were already exactly
17
+ what SWI produces, hashes included, which is now a test.
18
+
19
+ ### Added
20
+
21
+ - **`prolog-notebook run <file>…`** — consults every program cell, runs every query below it,
22
+ and writes the solution sequences back with an `input-hash` for each. `--limit`, `--stdout`,
23
+ `--quiet`. The logic is in `src/run.js` and takes a parsed notebook and a session, so
24
+ `--check` and a VS Code "run all" will get the same behaviour without a shell.
25
+ - **`--version`**, which says which SWI-Prolog will produce your answers. swipl-wasm 8.0.7
26
+ ships SWI-Prolog 10.1.13, and the two numbers are unrelated — so the engine version is the
27
+ one fact there that nobody could have looked up. A published install also reports the commit
28
+ it was built from; a working copy says so, and says when it has uncommitted edits.
29
+ - **A chapter is readable cold.** Saved answers render with no engine anywhere, and are marked
30
+ stale when the program above them has moved.
31
+ - **`hold`** — a query cell can withhold its saved answers until the reader runs it, or until
32
+ they have written the prediction above it. A page that has already printed all six answers is
33
+ arguing with prose that says "press Run".
34
+ - **`rerun="auto"`** — the answers follow the program. On consult, never on edit; never
35
+ starting work nobody asked for; and a held cell stays manual until its wait ends.
36
+ - **The page's own controls**: a lozenge that raises a card — what the engine is holding, the
37
+ chapter's answers shown or hidden, and the notebook itself. It says which version is on
38
+ screen, and once the two differ, lets you download either.
39
+ - **Per-cell reset**, on both runnable kinds. On a program cell that means out of the engine
40
+ as well as back to the chapter's text.
41
+ - **A stateful cell says so** before anything is asserted into it.
42
+ - **Download your own copy** — the reader leaves with a real `.prolog.md`, their edits and
43
+ their answers in it, hashed against the program that produced them.
44
+
45
+ ### Fixed
46
+
47
+ - **An answer containing variables was not what SWI would print.** `app([1,2], Tail, L)` came
48
+ out as `L = [1,2|_20306], Tail = _20428` where a toplevel prints `L = [1, 2|Tail]` — the
49
+ same variable shown as two, with the reader's own name for it discarded. Each binding was
50
+ rendered in a separate round trip into Prolog, and two round trips cannot share a variable.
51
+ Prolog now renders the whole answer, once, with the goal's own `variable_names`. Invisible
52
+ until now because every answer in the shipped chapter is a ground atom.
53
+ - **A half-walked query was destroyed by running any other cell.** SWI keeps open queries on a
54
+ stack; nothing here released one, and the next query nested inside it. There is now one open
55
+ sequence per engine, and the cell that loses its own is told so in words about the notebook
56
+ rather than about SWI's internals.
57
+ - **A `hold` release, a stuck Hide control, and a panel that resized under the cursor** — all
58
+ found by reading the page rather than the tests, which is why `src/notebook.js` now has a
59
+ jsdom harness and the page's behaviour is asserted from Node.
60
+ - **A compile error is reported in the cell's own terms**, not as a path the reader never
61
+ chose.
62
+
63
+ ### Changed
64
+
65
+ - **The engine is pinned exactly** (`swipl-wasm 8.0.7`). Two installs of one release, ten
66
+ minutes apart, ran SWI-Prolog 10.1.10 and 10.1.13 — and a chapter's saved answers are only
67
+ ever true of the engine that produced them. Moving the engine is now a commit with the
68
+ chapters re-run in it.
69
+ - Solutions are spelled as SWI's own writer spells them, so compounds gain a space after each
70
+ comma: `foo(1, 2)`, not `foo(1,2)`. Any file with saved answers containing a compound will
71
+ differ on its next `run`.
72
+ - `offerDownload()` takes an options object rather than positional arguments.
73
+ - The engine is imported where it is used rather than at the top of the CLI, so `--help` still
74
+ works on an install whose WebAssembly is missing.
75
+
76
+ ### Notes
77
+
78
+ `--check` — run a chapter in CI and fail the build when its answers have drifted — is
79
+ deliberately **not** here. It needs a timeout first: the Node engine runs in-process, so a
80
+ non-terminating goal hangs the command, and a test suite that can hang forever is not a test
81
+ suite. The command says so on every run.
82
+
83
+ ## [0.2.0] — 2026-08-16
84
+
85
+ **Breaking: the session API is asynchronous.** `consult`, `next` and `all` now return
86
+ promises, and in the browser the engine runs in a Web Worker.
87
+
88
+ ### Why
89
+
90
+ A Prolog query is synchronous WASM, so a goal that never terminates blocks the thread it
91
+ runs on. Measured, not assumed: `consult('loop :- loop.')` followed by `query('loop').next()`
92
+ stopped Node's event loop dead — a timer scheduled for 3 seconds never fired. In a browser
93
+ that is not a slow page, it is a dead one: no repaint, no button, no way back except closing
94
+ the tab.
95
+
96
+ That is unacceptable here specifically, because **non-termination is chapter material**. Left
97
+ recursion and a generator with no base case are things a Prolog book has to demonstrate, and
98
+ the demonstration has to be survivable.
99
+
100
+ ### Added
101
+
102
+ - **The page says what the engine is holding.** A program cell's tick carries the time it was
103
+ consulted (`title="consulted at 14:32:05"`, absolute — a relative time baked into an
104
+ attribute is wrong the moment it is written), and flips to **edited since consulted** as soon
105
+ as the textarea diverges from what was actually loaded. That state, not the clock, is the
106
+ reader's real question, and it became easy to get wrong the moment Run started consulting
107
+ cells by itself.
108
+ - **Reset, at two scales.** Per cell: restore the chapter's text *and re-consult it*, because
109
+ putting the page back without putting the engine back leaves them disagreeing. Page-level:
110
+ **restart engine**, which throws the worker away and replays the consult log — the documented
111
+ answer for a `:- dynamic` cell whose state lives in no file. Verified in a browser: a counter
112
+ mutated to 41 is 0 again afterwards. Per-cell *unconsult* is deliberately absent; it raises
113
+ "what happens to the cells that depended on it", and page-level restart has no such question.
114
+ - **The engine's state is visible**: *engine not started* until something needs it, which is
115
+ also the plainest evidence that a cold chapter is readable without it.
116
+ - **The chapter is readable before the engine arrives, and without it.** A query cell renders
117
+ the answers stored in the file — labelled as the chapter's, not the reader's (docs/modes.md
118
+ §3) — and the 5.9 MB WASM bundle is fetched only when someone presses Run. Verified in a
119
+ browser by watching the network: on a cold load there is no request for it at all. This is
120
+ what makes a published chapter degrade to a book rather than to a blank page.
121
+ - **Saved answers that no longer follow from the program above them are marked**, before first
122
+ paint and with no engine: the stored `input-hash` is compared against a 64-bit FNV-1a of the
123
+ goal and the program cells preceding it. Marked rather than hidden — never silently
124
+ discarded and never silently trusted.
125
+ - **A chapter is a file.** `prolog-notebook/page` fetches a `.prolog.md`, parses it, renders
126
+ the whole page and wires up the cells: `await load('chapter-04-cut.prolog.md')`. Program and
127
+ query cells are generated from the model rather than marked up by hand, which is what makes
128
+ writing a chapter *writing markdown*.
129
+ - **The `once/1` chapter is now that file**, and the hand-written page it was ported from is
130
+ deleted. Same prose, same four queries, same answers — six duplicated sons, one from `son_a`,
131
+ three from `son_b`, `true` for the ground goal — driven from
132
+ [`notebooks/ch04-cut.prolog.md`](notebooks/ch04-cut.prolog.md) instead of 200 lines of HTML.
133
+ It carries its saved answers and their `input-hash`es, and a test asserts the chapter agrees
134
+ with them, so an edit to a program cell that invalidates an answer below it fails CI.
135
+ On the repo page it reads as a document: prose as prose, Prolog syntax-highlighted, and the
136
+ prediction still hidden behind a `<details>` you have to click.
137
+ - **Run brings its own context.** Pressing Run on a query consults the program cells above it
138
+ first, so a reader who lands halfway down a chapter gets an answer rather than
139
+ `Unknown procedure`. Cells already loaded at their current text are skipped, so the second
140
+ Run of a chapter consults nothing and an edited cell invalidates only itself.
141
+ There is no dependency graph, and there is no need for one: **Prolog has no load-time name
142
+ binding** — `q(X) :- p(X)` merely mentions `p/1`, which is looked up when it is *called* — so
143
+ consult order cannot affect correctness, and at ~3.5 ms a cell there is nothing to gain by
144
+ computing one.
145
+ - **An error a reader can act on.** `wasm:wasm_call_string/3: Unknown procedure: son_a/1` is
146
+ our own plumbing in the middle of a lesson; it now reads `Unknown procedure: son_a/1`, and
147
+ where the predicate is defined by a cell *below* the query, the notebook says so and names
148
+ it. Context frames belonging to the reader's own code are untouched — `//2: Arithmetic:
149
+ evaluation error` still names the division that failed.
150
+ - `prolog-notebook/render` — the cell model to HTML **strings**, DOM-free. The same emitter
151
+ serves the browser today, the static build in v0.3 and the VS Code renderer later; one
152
+ implementation, four consumers.
153
+ - A generated program cell carries its notebook id as `data-cell`, and the consult is named by
154
+ it. SWI now says `Previously defined at /p-family.pl:20` — a warning that names a cell the
155
+ reader can find in the source, rather than `/cell-3.pl`.
156
+ - **`notebooks/` holds chapters and `viewer/` holds the one page that renders them.** They had
157
+ both been living in `example/`, under a script (`npm run example`, now `npm run dev`) that
158
+ actually served the repo root. A chapter is not an example, there will be many of them, and
159
+ the first one had been doubling as a parser fixture *with a deliberately wrong `input-hash`* —
160
+ which a file anybody is meant to read must never carry. The wrong hash now lives in
161
+ `test/fixtures/stale-output.prolog.md`, where being wrong is the point.
162
+ - **The engine runs in a Web Worker** in the browser, so a runaway goal costs a click on
163
+ Stop rather than the tab. Verified by driving Chrome: with `loop` spinning, timers still
164
+ fire, layout still runs, and the notebook is usable again afterwards.
165
+ - `session.abort()` and `session.restart()` — terminate the worker and replay the consults
166
+ into a new engine. Affordable only because one cell is one virtual file, so a chapter's
167
+ clause store rebuilds in milliseconds; terminating also reclaims the whole WASM heap, so a
168
+ memory blow-up and an infinite loop have the same cure.
169
+ - A `stop` button in the query cell, and a `ConsultLog` holding one entry per cell — the
170
+ latest text, not a history — so a replay restores what the reader actually has.
171
+ - `prolog-notebook/format` (0.1.3, listed here for completeness): the notebook parser,
172
+ canonical serialiser and `input-hash`.
173
+
174
+ ### Fixed
175
+
176
+ - **The browser session had no `restart()`**, while the in-process one and the README both did
177
+ — found by wiring a button to it. Both sessions now implement one interface, and a test
178
+ compares them, because nothing else could: Node never runs the worker session and a browser
179
+ never runs the other.
180
+ - `session.formatSolution()` is **removed** rather than added to the worker session. The
181
+ engine-backed one renders through SWI; a worker-backed one could only fall back to the
182
+ engine-free spelling, so the same call would have quietly meant two different things
183
+ depending on where it ran. Use `query.next().text`, or the exported `formatSolution()`.
184
+
185
+ ### Changed
186
+
187
+ - `createSession()` returns a session whose methods are all async. Migration is mechanical:
188
+ `session.consult(…)` → `await session.consult(…)`, `q.next()` → `await q.next()`.
189
+ - The browser page no longer loads the WASM bundle with a `<script>` tag; the worker loads
190
+ it. A page may pass `swiplUrl` if it lives somewhere unusual.
191
+ - Node still runs the engine in-process and is **not** protected against a runaway goal. The
192
+ CLI is not an interactive page; the limitation is documented rather than implied.
193
+
194
+ ### Notes
195
+
196
+ `assert`/`retract` state does not survive an abort, which is already the documented
197
+ behaviour of "restart engine and run all" (`docs/format.md` §8) rather than a new surprise.
198
+
199
+ ## [0.1.2] — 2026-08-04
200
+
201
+ Two bugs, both of which made the library quietly say something untrue. Found by
202
+ running the published entry point, not by reading the code.
203
+
204
+ ### Fixed
205
+
206
+ - **Compound terms lost their arguments.** `foo(1,2)` rendered as `foo()`,
207
+ `a-b` as `-()`, `f(g(h))` as `f()`. The arguments of a compound arrive under
208
+ the key *named by the functor* and wrapped in one extra array, not under
209
+ `args` as the formatter assumed. Atoms, numbers and lists were unaffected,
210
+ which is why the `once/1` example never showed it — that chapter only ever
211
+ binds variables to atoms.
212
+ - **A syntax error reported a successful consult.** SWI prints the offending
213
+ clause, skips it and carries on, so `consult/1` still succeeded and the cell
214
+ said `✓ consulted` while the predicate was not there.
215
+ - **A cell could silently destroy another cell's clauses.** Two cells defining
216
+ the same predicate make SWI print "Redefined static procedure" and keep only
217
+ the later one. That warning went to the console and nothing reached the page.
218
+ - `consult` no longer produces paths like `/chapter.pl.pl` when the cell name
219
+ already ends in `.pl`.
220
+
221
+ ### Added
222
+
223
+ - `PrologSession#formatTerm` and `#formatSolution` render through SWI itself, so
224
+ operators, quoting and every other rule of the writer come out right —
225
+ `X = a-b`, not `X = -(a, b)`. `query.next()` now carries a `text` field with
226
+ the solution already rendered this way; prefer it over `formatSolution`.
227
+ - `consult` returns `messages: [{kind, text}]` — SWI's warnings and errors for
228
+ that cell, captured through `message_hook/3`.
229
+ - `argumentsOf`, `textOf` and `toEngineTerm` are exported for anything that
230
+ needs to walk a term.
231
+ - Eleven more tests, including the reconsult behaviour the notebook renderer
232
+ will depend on: re-consulting one cell replaces exactly that cell's clauses,
233
+ leaves dependent cells working, and leaves no ghost behind when a predicate is
234
+ renamed.
235
+
3
236
  ## [0.1.1] — 2026-08-02
4
237
 
5
238
  No functional change. Published from CI via npm trusted publishing (OIDC) to
package/README.md CHANGED
@@ -32,7 +32,7 @@ the reader clicks a link.
32
32
  The reason this is not "Jupyter with a different kernel" is the button marked `; next`.
33
33
 
34
34
  Jupyter's model is request/response: run a cell, get a result. Prolog's model is a stream of
35
- solutions you walk through. In the included example, `is_son(X)` reports edward *twice* — and
35
+ solutions you walk through. In the included chapter, `is_son(X)` reports edward *twice* — and
36
36
  that duplication **is the lesson**, because it means Prolog found two proofs. A notebook that
37
37
  showed only a final list of results would have hidden the very thing worth teaching.
38
38
 
@@ -44,15 +44,63 @@ So a query cell gives you the first solution, and then you step.
44
44
  git clone https://github.com/jarecsni/prolog-notebook
45
45
  cd prolog-notebook
46
46
  npm install
47
- npm run example # then open http://localhost:8777/example/
47
+ npm run dev # serves the repo root on :8777
48
48
  ```
49
49
 
50
- The example is a real worked section — the `once/1` placement puzzle — not a widget demo.
51
- Predict what each version returns before you press Run.
50
+ Then open **http://localhost:8777/viewer/**. That is a chapter — the `once/1` placement puzzle,
51
+ a real worked section rather than a widget demo — rendered from
52
+ [`notebooks/ch04-cut.prolog.md`](notebooks/ch04-cut.prolog.md). Predict what each version
53
+ returns before you press Run.
52
54
 
53
- It has to be **served over HTTP**. Opening `example/index.html` straight from disk leaves the
54
- buttons inert, because browsers block ES modules over `file://` the page detects this and
55
- says so rather than failing silently.
55
+ Edit that file, reload, and the chapter changes: prose, Prolog, margin note and prediction box
56
+ are all in it, and there is no HTML anywhere. Point the viewer at any other notebook with
57
+ `?src=`.
58
+
59
+ | | |
60
+ |---|---|
61
+ | `notebooks/` | chapters. The product. |
62
+ | `viewer/` | one shell page that renders any of them. Replaced by `build` in v0.3. |
63
+
64
+ The chapter also reads on the repo page, [as a file](notebooks/ch04-cut.prolog.md), with no
65
+ build step and no site: prose as prose, Prolog syntax-highlighted, the saved answers in place,
66
+ and the prediction still hidden behind a `<details>` you have to click. That is the whole
67
+ reason the format is markdown.
68
+
69
+ It has to be **served over HTTP**. Opening the page straight from disk leaves the buttons
70
+ inert, because browsers block ES modules over `file://` — the page detects this and says so
71
+ rather than failing silently.
72
+
73
+ ## Fill in a chapter's answers
74
+
75
+ A chapter's saved answers have to come from a real run — a hand-written output block is the
76
+ author's *guess* at what SWI prints, published as though it ran. So write the file with no
77
+ output blocks and let the engine fill them in:
78
+
79
+ ```sh
80
+ npx prolog-notebook run notebooks/ch04-cut.prolog.md
81
+ ```
82
+
83
+ It consults every program cell, runs every query below it, and writes the solution sequences
84
+ back into the file along with an `input-hash` for each — which is what makes the chapter render
85
+ complete, and render as *current*, before the engine arrives. Running it again on an unchanged
86
+ chapter changes nothing.
87
+
88
+ | flag | |
89
+ |---|---|
90
+ | `--limit <n>` | solutions to take from one query before stopping. Default 100. |
91
+ | `--stdout` | print the result instead of writing the file |
92
+ | `--quiet` | report only failures |
93
+ | `--version` | the tool's version, **the SWI-Prolog version it will run your chapters with**, and the copyright |
94
+
95
+ Two things it will not do. A query stopped at the limit is written **without** a terminator,
96
+ which is the format's way of saying the search was never exhausted — `false.` there would be a
97
+ forgery. And if a program cell fails to load, nothing is written at all: every answer below it
98
+ was produced against a chapter that does not exist.
99
+
100
+ It has no defence against a non-terminating goal yet — the engine runs in this process, so
101
+ `loop :- loop.` hangs the command. Say the word `--limit` all you like; a runaway *consult* is
102
+ not a solution count. Fixing it properly means a worker thread, and it is the prerequisite for
103
+ putting this in CI.
56
104
 
57
105
  ## Use it
58
106
 
@@ -62,36 +110,70 @@ Headless, in Node — this is how you test that every example in a document stil
62
110
  import { createSession, formatSolution } from 'prolog-notebook';
63
111
 
64
112
  const session = await createSession();
65
- session.consult(`
113
+ await session.consult(`
66
114
  male(edward). male(alfred).
67
115
  father(albert, edward). mother(victoria, edward).
68
116
  parent(X, Y) :- father(X, Y) ; mother(X, Y).
69
117
  is_son(X) :- male(X), parent(_, X).
70
- `);
118
+ `, 'cell-family');
71
119
 
72
120
  // Step solutions one at a time, as at the prompt
73
121
  const q = session.query('is_son(X)');
74
122
  let r;
75
- while (!(r = q.next()).done) console.log(formatSolution(r.solution));
123
+ while (!(r = await q.next()).done) console.log(r.text);
76
124
 
77
125
  // …or drain it
78
- const { solutions } = session.query('is_son(X)').all();
126
+ const { solutions } = await session.query('is_son(X)').all();
79
127
  ```
80
128
 
81
- In a browser, load the WASM bundle with a `<script>` tag, then import from
82
- `prolog-notebook/browser`. See [`example/index.html`](example/index.html) for the cell
83
- markup and [`src/notebook.js`](src/notebook.js) for the wiring.
129
+ **Every call is awaited**, because in the browser the engine runs in a Web Worker. A Prolog
130
+ query is synchronous WASM, so a goal that never terminates blocks whatever thread it is on —
131
+ and non-termination is *chapter material* in a Prolog book. Running the engine somewhere that
132
+ can be terminated is what turns `loop :- loop.` from a dead tab into a Stop button.
133
+
134
+ In a browser, import from `prolog-notebook/browser` and let it start the worker; the page does
135
+ **not** need a `<script>` tag for the WASM bundle any more, because the worker loads it. See
136
+ [`viewer/index.html`](viewer/index.html) for the whole of a host page, and
137
+ [`src/notebook.js`](src/notebook.js) for the wiring.
138
+
139
+ ```js
140
+ import { createSession } from 'prolog-notebook/browser';
141
+ const session = await createSession(); // boots the worker
142
+ await session.abort(); // stop a runaway goal; cells are re-consulted
143
+ ```
144
+
145
+ To render a notebook file instead of marking up cells by hand — the whole page from one call:
146
+
147
+ ```js
148
+ import { load } from 'prolog-notebook/page';
149
+ await load('chapter-04-cut.prolog.md'); // parse, render into <main>, wire up the cells
150
+ ```
151
+
152
+ `prolog-notebook/format` (parse, serialise, `inputHash`) and `prolog-notebook/render` (model to
153
+ HTML strings) are exported separately, and neither touches the DOM — the same two modules back
154
+ the browser, the CLI runner and a future VS Code serializer.
155
+
156
+ Node runs the engine in-process and is deliberately **not** protected: a non-terminating goal
157
+ will hang it. The CLI is not an interactive page, and pretending otherwise would hide the
158
+ difference.
84
159
 
85
160
  ### API
86
161
 
87
162
  | | |
88
163
  |---|---|
89
- | `createSession(options?)` | boots SWI-Prolog; returns a `PrologSession` |
90
- | `session.consult(text, name?)` | loads a clause base into `user`; `{ok, error?}` |
91
- | `session.query(goal)` | opens a query; returns a `PrologQuery` |
92
- | `query.next()` | one solution: `{done, solution?, error?}` |
93
- | `query.all(limit?)` | drains it: `{solutions, error?, truncated}` |
94
- | `formatSolution(s)` | renders bindings the way a top level would |
164
+ | `createSession(options?)` | boots SWI-Prolog; returns a session |
165
+ | `await session.consult(text, name?)` | loads a clause base into `user`; `{ok, error?, messages}` |
166
+ | `session.query(goal)` | opens a query; nothing runs until you pull a solution |
167
+ | `await query.next()` | one solution: `{done, solution?, text?, error?}` |
168
+ | `await query.all(limit?)` | drains it: `{solutions, error?, truncated}` |
169
+ | `await session.abort()` | terminates a running goal and replays the consults |
170
+ | `await session.restart()` | same, without anything needing to be running |
171
+ | `formatSolution(s)` | renders bindings the way a top level would, with no engine |
172
+
173
+ Abort is cheap because of a decision made elsewhere: one cell is one virtual file, so the
174
+ clause store rebuilds from the cells in milliseconds. Terminating the worker also reclaims the
175
+ whole WASM heap, so a runaway loop and a memory blow-up have the same cure. Assert/retract
176
+ state does not survive it — see [`docs/format.md`](docs/format.md) §8.
95
177
 
96
178
  ## Two things that will bite you if you build this yourself
97
179
 
@@ -99,7 +181,19 @@ Both were found by driving a real browser, not by reading documentation.
99
181
 
100
182
  **Module context.** `prolog.query(Goal)` runs with `system` as its context module, while
101
183
  `consult/1` loads into `user`. Unqualified goals raise `Unknown procedure: system:foo/1`
102
- *even though the consult reported success*. Goals are wrapped as `user:( Goal )`.
184
+ *even though the consult reported success*. Goals are read and called in `user`.
185
+
186
+ **One round trip per answer, or the variables come apart.** Formatting each binding with its
187
+ own `term_string/2` call loses the fact that two of them are the *same* variable:
188
+ `app([1,2], Tail, L)` came out as `L = [1,2|_20306], Tail = _20428` where a real toplevel
189
+ prints `L = [1, 2|Tail]`. Render the whole answer inside Prolog, in one call, with the goal's
190
+ own `variable_names`.
191
+
192
+ **The engine version is not the package version, and it must not float.**
193
+ `swipl-wasm@8.0.4` ships SWI-Prolog 10.1.10; `8.0.7` ships 10.1.13. Two installs of the same
194
+ release, ten minutes apart, ran different Prologs. Since a notebook's saved answers are only
195
+ true of the engine that produced them — and SWI's answer spelling changes between releases —
196
+ the dependency is pinned exactly, and moving it is a commit with the chapters re-run in it.
103
197
 
104
198
  **The last solution arrives with `done`.** `next()` can return `{done: true, value: {...}}` —
105
199
  a final binding and the end of the search in a single step. Treating `done` as "stop, no more
@@ -110,18 +204,27 @@ lesson about backtracking quietly teaches the wrong thing.
110
204
 
111
205
  Working and tested:
112
206
 
113
- - execution core, environment-agnostic (`src/engine.js`), 8 passing tests
207
+ - execution core, environment-agnostic (`src/engine.js`), run in a Web Worker in the browser
114
208
  - Node entry point, browser entry point
115
- - program cells and query cells with `Run` / `; next` / `all`
116
- - the worked example
209
+ - **a chapter is a file** parse, render and mount a `.prolog.md`, cells and all
210
+ - **a chapter is readable cold** — saved answers render with no engine, and are marked stale
211
+ when the program above them has moved
212
+ - program cells and query cells with `Run` / `; next` / `all` / `stop`, per-cell reset, and a
213
+ page that says what the engine is holding
214
+ - `hold` and `rerun="auto"` — the author decides what a reader may see and when it refreshes
215
+ - **the CLI runner**: `prolog-notebook run` executes a chapter headlessly and writes its
216
+ answers back
217
+ - download your own copy of a chapter, answers and all
218
+ - 186 passing tests
117
219
 
118
220
  Not built yet:
119
221
 
120
- - **a file-backed renderer** reading `.ipynb` or markdown and *generating* the cells.
121
- Today the example's cells are hand-written HTML. This is the next real piece of work.
222
+ - `--check` run a chapter in CI and fail the build when its answers have drifted. Needs a
223
+ timeout first: a test suite that can hang forever is not a test suite.
224
+ - `build` — a static page a reader can open, without a dev server
122
225
  - custom elements (`<prolog-program>`, `<prolog-query>`) so notebooks drop into any static site
123
226
  - a VS Code notebook controller — VS Code supplies the UI, this supplies the kernel, still no Python
124
- - a CLI runner, to execute a document's cells in CI and fail the build when an example rots
227
+ - persistence, so a reader's edits survive a reload
125
228
  - `trace/0` integration, for visible backtracking — where [prolog-trace-viz][ptv] would plug in
126
229
  - syntax highlighting
127
230
 
@@ -140,4 +243,4 @@ MIT
140
243
 
141
244
  [kernel]: https://github.com/hhu-stups/prolog-jupyter-kernel
142
245
  [wasm]: https://github.com/SWI-Prolog/swipl-wasm
143
- [ptv]: https://github.com/jarecsni/prolog-trace-viz
246
+ [ptv]: https://github.com/textologylabs/prolog-trace-viz