prolog-notebook 0.6.3 → 0.6.5

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,52 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.6.5] — 2026-08-31
4
+
5
+ Both from one field report on a real chapter.
6
+
7
+ ### Fixed
8
+
9
+ - **`Attempt to access not innermost query` on a perfectly ordinary Run.** Loading the program
10
+ cells above a query emits `consulted`, which is exactly what `rerun="auto"` waits for — so an
11
+ auto cell's re-run was queued and ran while the manual run was still inside its awaits. It
12
+ asked whether anybody was mid-sequence, saw nobody, and opened its query underneath the one
13
+ about to be opened. One engine allows one open query, and whichever is not innermost cannot
14
+ be stepped, so the reader — who had pressed Run on one cell and touched nothing else — got
15
+ SWI's own words for a stack they are never supposed to meet.
16
+
17
+ A cell now claims the engine when its run **starts**, not when it succeeds. Two supporting
18
+ changes close the same class: the engine's version probe is awaited during boot rather than
19
+ fired beside the reader's first Run, and `all()` holds the session's one slot until the
20
+ worker has answered rather than releasing it up front.
21
+
22
+ - **No way to clear your own answers in a chapter that shipped without any.** The outputs row
23
+ removed itself when the FILE had no saved answers — but that argument dies the moment a
24
+ reader presses Run, and a chapter published unrun is most chapters while an author is still
25
+ writing. The row now exists whenever there is a query cell, and counts what is on the page
26
+ rather than what is in the file: *No outputs yet*, *3 outputs on this page*, *3 outputs
27
+ cleared*.
28
+
29
+ The hide row above it stays keyed to the file, and correctly — hiding is for the chapter's
30
+ saved answers, and a reader's own run is not a spoiler.
31
+
32
+ ## [0.6.4] — 2026-08-31
33
+
34
+ ### Changed
35
+
36
+ - **`prolog-notebook <command> --help` answers about that command only.** It printed the whole
37
+ card — all five commands — which makes the reader find their command again in a page they did
38
+ not ask for. What works anywhere is still listed, because it is as true of the command they
39
+ asked about as of any other, and the note about a stopped search now travels with `--limit`
40
+ rather than sitting under everything.
41
+
42
+ It works wherever the command is named: `view --help`, `--help view`, and
43
+ `view chapter.prolog.md -h` all ask the same question, as do the `run` and `exec` aliases.
44
+ Name no command, or one that does not exist, and the whole card comes back as before.
45
+
46
+ - **The help, the per-command help and the misplaced-option message are derived from one
47
+ table.** They were three lists kept by hand, which is how the help came to advertise
48
+ `--check-update` under no command while three of the four commands refused it.
49
+
3
50
  ## [0.6.3] — 2026-08-30
4
51
 
5
52
  `prolog-notebook build lists.prolog.md --check-update` answered *unknown option* — on a flag the
package/README.md CHANGED
@@ -149,7 +149,7 @@ Three work anywhere, because they are about the tool rather than about a noteboo
149
149
  |---|---|
150
150
  | `--version` | the tool's version, **the SWI-Prolog version it will run your chapters with**, and the copyright |
151
151
  | `--check-update` | ask npm whether a newer one exists, and say so either way — forced, whatever the daily check thinks, and answered even down a pipe |
152
- | `-h`, `--help` | the same list, from the command itself |
152
+ | `-h`, `--help` | help for the command you named, or all of them if you named none |
153
153
 
154
154
  ```sh
155
155
  prolog-notebook upgrade # fetch the latest
@@ -38,57 +38,112 @@ for (const stream of [process.stdout, process.stderr]) {
38
38
  const require = createRequire(import.meta.url);
39
39
 
40
40
  /**
41
- * EACH OPTION UNDER THE COMMAND THAT TAKES IT (869erqra0).
41
+ * THE COMMANDS, AND WHAT EACH ONE TAKES one table, three readers (869erqra0).
42
42
  *
43
- * They were listed in one flat `Options` block with no owner, which reads as a
44
- * promise that every one of them works everywhere so `build --check-update`
45
- * was a reasonable thing to type and `unknown option` was a strange thing to be
46
- * told. Only three of them are actually the tool's rather than a command's, and
47
- * now only those three are listed as such.
43
+ * The whole help, a single command's help, and the message a misplaced option
44
+ * gets are all derived from here. They were three lists kept by hand, which is
45
+ * how the help came to advertise flags that no command accepted.
46
+ *
47
+ * Options belong to COMMANDS, not to the tool. Listing them in one flat block
48
+ * reads as a promise that every one works everywhere, and only three of them do.
48
49
  */
49
- const USAGE = `prolog-notebook — Jupyter-style notebooks for Prolog
50
-
51
- prolog-notebook view <file.prolog.md> read it in a browser, cells and all
52
- --port <n> what it listens on (default 8777)
53
- --no-open print the URL instead of opening a browser
54
-
55
- prolog-notebook build <file.prolog.md> write a page you can host or send
56
- --out <dir> where it writes (default: <file>-site)
57
-
58
- prolog-notebook execute <file.prolog.md>... run every query, write the answers in
59
- --limit <n> solutions to take from one query before stopping (default ${DEFAULT_LIMIT})
60
- --stdout print the result instead of writing the file
61
- --quiet report only failures
50
+ const COMMANDS = {
51
+ view: {
52
+ usage: 'prolog-notebook view <file.prolog.md>',
53
+ blurb: 'read it in a browser, cells and all',
54
+ options: [
55
+ ['--port <n>', 'what it listens on (default 8777)'],
56
+ ['--no-open', 'print the URL instead of opening a browser'],
57
+ ],
58
+ },
59
+ build: {
60
+ usage: 'prolog-notebook build <file.prolog.md>',
61
+ blurb: 'write a page you can host or send',
62
+ options: [['--out <dir>', 'where it writes (default: <file>-site)']],
63
+ },
64
+ execute: {
65
+ usage: 'prolog-notebook execute <file.prolog.md>...',
66
+ blurb: 'run every query, write the answers in',
67
+ options: [
68
+ ['--limit <n>', `solutions to take from one query before stopping (default ${DEFAULT_LIMIT})`],
69
+ ['--stdout', 'print the result instead of writing the file'],
70
+ ['--quiet', 'report only failures'],
71
+ ],
72
+ // Belongs to --limit, so it goes wherever --limit goes and nowhere else.
73
+ note: 'A query that stops at the limit is written without a terminator, which is the\n'
74
+ + "format's way of saying the search was never exhausted. Nothing is invented.\n",
75
+ },
76
+ clear: {
77
+ usage: 'prolog-notebook clear <file.prolog.md>...',
78
+ blurb: 'take the answers back out',
79
+ options: [
80
+ ['--stdout', 'print the result instead of writing the file'],
81
+ ['--quiet', 'report only failures'],
82
+ ],
83
+ },
84
+ upgrade: {
85
+ usage: 'prolog-notebook upgrade',
86
+ blurb: 'fetch the latest version',
87
+ options: [],
88
+ },
89
+ };
62
90
 
63
- prolog-notebook clear <file.prolog.md>... take the answers back out
64
- --stdout print the result instead of writing the file
65
- --quiet report only failures
91
+ /** `exec` and `run` reach `execute` and are deliberately undocumented (869erp0jd). */
92
+ const ALIASES = { exec: 'execute', run: 'execute' };
66
93
 
67
- prolog-notebook upgrade fetch the latest version
94
+ /** The command this argument names, aliases resolved, or null. */
95
+ const commandNamed = (arg) => (COMMANDS[arg] ? arg : ALIASES[arg] ?? null);
68
96
 
69
- Anywhere
97
+ const ANYWHERE = `Anywhere
70
98
  --check-update ask npm whether a newer one exists, and say so either way
71
99
  --version version, engine and copyright
72
100
  -h, --help this
73
-
74
- A query that stops at the limit is written without a terminator, which is the
75
- format's way of saying the search was never exhausted. Nothing is invented.
76
101
  `;
77
102
 
103
+ /** One command, laid out exactly as it is laid out in the full help. */
104
+ function commandHelp(name) {
105
+ const { usage, blurb, options } = COMMANDS[name];
106
+ return [` ${usage.padEnd(44)}${blurb}`]
107
+ .concat(options.map(([flag, what]) => ` ${flag.padEnd(16)}${what}`))
108
+ .join('\n');
109
+ }
110
+
111
+ const USAGE = `prolog-notebook — Jupyter-style notebooks for Prolog
112
+
113
+ ${Object.keys(COMMANDS).map(commandHelp).join('\n\n')}
114
+
115
+ ${ANYWHERE}
116
+ ${COMMANDS.execute.note}`;
117
+
118
+ /**
119
+ * JUST THE COMMAND ASKED ABOUT (869erqra0).
120
+ *
121
+ * The Captain, on being shown all five for `build --help`: "not really. If I run
122
+ * prolog-notebook cmd --help I want only help on that cmd." Printing everything
123
+ * makes the reader find their command again in a page they did not ask for.
124
+ *
125
+ * What works anywhere stays, because it is true of the command they asked about
126
+ * as much as of any other.
127
+ */
128
+ function helpFor(name) {
129
+ const { note } = COMMANDS[name];
130
+ return `${commandHelp(name)}\n\n${ANYWHERE}${note ? `\n${note}` : ''}`;
131
+ }
132
+
78
133
  /**
79
134
  * Which command each option belongs to, so a misplaced one can say where it lives.
80
135
  *
136
+ * Derived, so a flag added to a command above cannot be forgotten here.
81
137
  * `unknown option "--limit"` is true and unhelpful when the flag is real and two
82
- * lines further up the same help. This costs a lookup table and saves a re-read.
138
+ * lines further up the same help.
83
139
  */
84
- const BELONGS_TO = {
85
- '--limit': 'execute',
86
- '--stdout': 'execute and clear',
87
- '--quiet': 'execute and clear',
88
- '--out': 'build',
89
- '--port': 'view',
90
- '--no-open': 'view',
91
- };
140
+ const BELONGS_TO = {};
141
+ for (const [name, { options }] of Object.entries(COMMANDS)) {
142
+ for (const [flag] of options) {
143
+ const bare = flag.split(' ')[0];
144
+ BELONGS_TO[bare] = BELONGS_TO[bare] ? `${BELONGS_TO[bare]} and ${name}` : name;
145
+ }
146
+ }
92
147
 
93
148
  function unknownOption(arg, command) {
94
149
  const home = BELONGS_TO[arg];
@@ -237,7 +292,11 @@ const NPM_LINE = process.platform === 'win32' ? 'npm.cmd' : 'npm';
237
292
  async function main(argv) {
238
293
  const args = argv.slice(2);
239
294
  if (!args.length || args.includes('-h') || args.includes('--help')) {
240
- process.stdout.write(USAGE);
295
+ // Whichever command they named, wherever they named it: `view --help`,
296
+ // `--help view` and `view chapter.prolog.md -h` all ask the same question.
297
+ // A help flag that is positional is its own small annoyance.
298
+ const named = args.map((arg) => commandNamed(arg)).find(Boolean);
299
+ process.stdout.write(named ? helpFor(named) : USAGE);
241
300
  return 0;
242
301
  }
243
302
  if (args.includes('--version') || args.includes('-V')) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prolog-notebook",
3
- "version": "0.6.3",
3
+ "version": "0.6.5",
4
4
  "description": "Jupyter-style notebooks for Prolog. Runs in the browser, installs nothing.",
5
5
  "type": "module",
6
6
  "main": "./src/node.js",
package/src/browser.js CHANGED
@@ -229,8 +229,16 @@ class WorkerQuery {
229
229
  async all(limit) {
230
230
  if (this.done) return { solutions: [], truncated: false };
231
231
  const qid = await this.#open();
232
- this.#finish();
233
- return this.session.send('all', { qid, limit });
232
+ try {
233
+ return await this.session.send('all', { qid, limit });
234
+ } finally {
235
+ // AFTER THE WORKER HAS ANSWERED, not before. Releasing the session's one
236
+ // slot up front said "nothing is open here" while the engine was still
237
+ // inside the goal, so anything that opened next nested inside a frame the
238
+ // session had already forgotten (869erqvzu). The slot is the claim that
239
+ // this query is the innermost one, and that stays true until it is done.
240
+ this.#finish();
241
+ }
234
242
  }
235
243
 
236
244
  async close({ superseded = false } = {}) {
@@ -1,4 +1,4 @@
1
1
  {
2
- "commit": "2d089b9",
3
- "built": "2026-08-30 22:55:39 UTC"
2
+ "commit": "60e27bc",
3
+ "built": "2026-08-30 23:44:28 UTC"
4
4
  }
package/src/notebook.js CHANGED
@@ -478,11 +478,18 @@ function mountPageBar(root, options, bus, programs, queries) {
478
478
  const outputsUnit = bar.querySelector('.unit.outputs');
479
479
  const outputsState = bar.querySelector('.outputs-state');
480
480
  let refreshOutputs = () => {};
481
- // Nothing published to clear, and nothing a restore could give back. The row
482
- // goes, exactly as the one above it does — an unrun chapter is the CLI's
483
- // business, and a control whose only possible effect is on the reader's own
484
- // run is a control offering to undo the thing they just asked for.
485
- if (!spoilers.length) {
481
+ // ANY QUERY CELL AT ALL, not just a chapter that shipped answers.
482
+ //
483
+ // This used to go when the FILE had none, on the argument that an unrun
484
+ // chapter is the CLI's business. That argument was wrong the moment a reader
485
+ // pressed Run: a chapter published without answers — which is most of them
486
+ // while an author is still writing — gave them no way to clear the answers
487
+ // they had just produced, and a panel with the row missing reads as a control
488
+ // that has broken rather than one that had nothing to do (869erqw08).
489
+ //
490
+ // The row above stays keyed to the FILE, and correctly: hiding is for the
491
+ // chapter's saved answers, and a reader's own run is not a spoiler.
492
+ if (!queries.length) {
486
493
  outputsUnit.remove();
487
494
  } else {
488
495
  const wipe = document.createElement('button');
@@ -503,12 +510,14 @@ function mountPageBar(root, options, bus, programs, queries) {
503
510
  // to give back, so a page of only those leaves restore with no work.
504
511
  const restorable = gone.filter((q) => q.hasSaved).length;
505
512
  wipe.disabled = back ? restorable === 0 : left.length === 0;
506
- // COUNTED FROM THE FILE while nothing is cleared, because that is the fact
507
- // the reader does not have: how much of this chapter is answers. After
508
- // that it counts what they did, which is the fact they want confirmed.
509
- outputsState.textContent = gone.length === 0
510
- ? `${plural(spoilers.length, 'output')} in this chapter`
511
- : `${plural(gone.length, 'output')} cleared`;
513
+ // COUNTED FROM THE PAGE, which is the only count that stays true for both
514
+ // kinds of chapter: one published with its answers in it, and one still
515
+ // being written where every output on screen is the reader's own.
516
+ outputsState.textContent = gone.length > 0
517
+ ? `${plural(gone.length, 'output')} cleared`
518
+ : left.length > 0
519
+ ? `${plural(left.length, 'output')} on this page`
520
+ : 'No outputs yet';
512
521
  label(wipe, back ? 'restore' : 'erase',
513
522
  back ? 'Restore outputs' : 'Clear all outputs');
514
523
  wipe.title = back
@@ -667,12 +676,22 @@ async function boot(options, bus, status) {
667
676
  bus.booted = true;
668
677
  if (!wasBooted) {
669
678
  bus.emit({ kind: 'started', at: clock() });
670
- // Asked for, not waited for. The light going green is the answer to "did it
671
- // start", and holding that back for a round trip about a version number
672
- // would be the page reporting the less interesting fact first.
673
- prologVersion(session)
674
- .then((version) => version && bus.emit({ kind: 'engine-version', version }))
675
- .catch(() => {});
679
+ // AWAITED, AND THAT IS A CORRECTNESS MATTER RATHER THAN A PREFERENCE.
680
+ //
681
+ // This was fired and forgotten, on the argument that the light going green
682
+ // answers "did it start" and a version number should not hold it back. But
683
+ // asking for the version IS A QUERY, and one engine allows ONE OPEN QUERY
684
+ // (869epzqpc). Firing it alongside the reader's own Run put two opens into
685
+ // the worker's queue, and whichever arrived second nested inside the first —
686
+ // so the cell that started the engine got `Attempt to access not innermost
687
+ // query` while the panel cheerfully displayed the version that had won the
688
+ // race (869erqvzu).
689
+ //
690
+ // The light still goes green first: the event above is emitted before this
691
+ // waits. What it now costs is one flag lookup on the first boot only, and
692
+ // what it buys is that no cell query can ever be in flight beside it.
693
+ const version = await prologVersion(session).catch(() => null);
694
+ if (version) bus.emit({ kind: 'engine-version', version });
676
695
  }
677
696
  return session;
678
697
  }
@@ -1227,6 +1246,20 @@ function mountQuery(cell, options, bus, { above = [], below = [], prediction = n
1227
1246
  : whose, 'from');
1228
1247
  write(`?- ${goal}.`, 'echo');
1229
1248
  setRunning(true);
1249
+ // THE ENGINE IS CLAIMED HERE, BEFORE THE CONSULTS — not after the query is
1250
+ // opened, which is where this used to be (869erqvzu).
1251
+ //
1252
+ // Loading the cells above emits `consulted`, and that is exactly what an
1253
+ // auto cell waits for. Its re-run is queued and runs on the next turn, while
1254
+ // this run is still somewhere inside its awaits — so it looked at a page
1255
+ // where nobody was stepping, saw a free engine, and opened its own query
1256
+ // underneath the one this cell was about to open. The reader, who had
1257
+ // pressed Run on one cell and touched nothing else, got `Attempt to access
1258
+ // not innermost query`.
1259
+ //
1260
+ // A cell is using the engine from the moment it starts, not from the moment
1261
+ // it succeeds. finish() lets go, on every path out of here.
1262
+ bus.stepping.add(cell);
1230
1263
  try {
1231
1264
  if (!bus.booted) write('starting SWI-Prolog (5.9 MB, first time only)…', 'done');
1232
1265
  session = await boot(options, bus);
@@ -1243,7 +1276,6 @@ function mountQuery(cell, options, bus, { above = [], below = [], prediction = n
1243
1276
  return;
1244
1277
  }
1245
1278
  query = session.query(goal);
1246
- bus.stepping.add(cell);
1247
1279
  // A sequence ends when another one starts — one engine, one open query
1248
1280
  // (869epzqpc). The reader hears it here, in the cell it happened to, with
1249
1281
  // the solutions they did take still under it and Run still lit. Silence was
package/src/version.js CHANGED
@@ -10,7 +10,7 @@
10
10
  export const NAME = 'Prolog Notebook';
11
11
 
12
12
  /** Must equal package.json's `version` — test/run.test.mjs enforces it. */
13
- export const VERSION = '0.6.3';
13
+ export const VERSION = '0.6.5';
14
14
 
15
15
  /** The two facts a licence notice is actually made of. */
16
16
  export const YEAR = '2026';