geneseed 3.1.3 → 3.2.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.
Files changed (113) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/README.md +2 -2
  3. package/SHIPPED.md +3 -3
  4. package/bin/build-driver.mjs +127 -79
  5. package/bin/geneseed-cli.mjs +42 -84
  6. package/bin/geneseed-hook.mjs +5 -2
  7. package/harness.config.json +1 -1
  8. package/js/README.md +1 -10
  9. package/js/build/emit-claude.mjs +4 -5
  10. package/js/build/emit-common.mjs +63 -114
  11. package/js/build/emit-opencode.mjs +74 -66
  12. package/js/build/generate.mjs +4 -6
  13. package/js/build/render.mjs +13 -13
  14. package/js/build/source.mjs +7 -6
  15. package/js/build/stubs.mjs +25 -33
  16. package/js/build/themes.mjs +1 -1
  17. package/js/build/version.mjs +1 -3
  18. package/js/hosts/hooks.mjs +3 -3
  19. package/js/hosts/hosts.mjs +2 -4
  20. package/js/hosts/installs.mjs +35 -28
  21. package/js/hosts/link.mjs +1 -1
  22. package/js/hosts/native.mjs +37 -19
  23. package/js/hosts/opencode.mjs +39 -28
  24. package/js/hosts/settings.mjs +33 -110
  25. package/js/inspect/checks-authoring.mjs +5 -4
  26. package/js/inspect/checks-build.mjs +1 -1
  27. package/js/inspect/diff.mjs +1 -3
  28. package/js/inspect/doctor.mjs +59 -44
  29. package/js/inspect/excludes.mjs +4 -10
  30. package/js/inspect/inventory.mjs +37 -55
  31. package/js/inspect/registry.mjs +4 -7
  32. package/js/inspect/scan.mjs +8 -15
  33. package/js/inspect/status.mjs +1 -1
  34. package/js/lib/fs.mjs +34 -0
  35. package/js/lib/json.mjs +28 -1
  36. package/js/lib/paths.mjs +0 -10
  37. package/js/lib/udiff.mjs +11 -16
  38. package/js/maintain/memory.mjs +2 -4
  39. package/js/maintain/migrate.mjs +3 -3
  40. package/js/maintain/uninstall.mjs +90 -78
  41. package/js/maintain/update.mjs +2 -2
  42. package/js/ui/anim.mjs +1 -1
  43. package/js/ui/menu.mjs +1 -1
  44. package/js/ui/tui.mjs +3 -20
  45. package/js/web/actions.mjs +230 -217
  46. package/js/web/activity.mjs +71 -89
  47. package/js/web/api.mjs +133 -224
  48. package/js/web/daemon.mjs +92 -110
  49. package/js/web/docs.mjs +66 -111
  50. package/js/web/graph.mjs +0 -0
  51. package/js/web/handler.mjs +78 -97
  52. package/js/web/jobs.mjs +124 -165
  53. package/js/web/routes.mjs +73 -152
  54. package/js/web/server.mjs +47 -82
  55. package/package.json +1 -1
  56. package/src/agents/explorer.md +2 -1
  57. package/src/doctrines/process.md +4 -2
  58. package/src/laws/universal.md +55 -43
  59. package/src/skills/clarify.md +1 -1
  60. package/src/skills/forge-mcp.md +2 -1
  61. package/src/skills/fresh-eyes.md +1 -1
  62. package/src/skills/ingest.md +2 -1
  63. package/src/skills/parallel-agents.md +1 -1
  64. package/src/skills/ponytail.md +2 -2
  65. package/src/skills/research.md +4 -2
  66. package/themes/_TEMPLATE.json +2 -0
  67. package/themes/biker.json +2 -0
  68. package/themes/commentator.json +2 -0
  69. package/themes/cyberpunk.json +2 -0
  70. package/themes/gamer.json +2 -0
  71. package/themes/imperial.json +2 -0
  72. package/themes/joker.json +2 -0
  73. package/themes/marvin.json +2 -0
  74. package/themes/mean.json +2 -0
  75. package/themes/military.json +2 -0
  76. package/themes/neutral.json +2 -0
  77. package/themes/pirate.json +2 -0
  78. package/themes/sports.json +2 -0
  79. package/themes/verstappen.json +2 -0
  80. package/themes/wizard.json +2 -0
  81. package/web/dist/assets/{Activity-B49fE_ug.js → Activity-BTCG2gMl.js} +1 -1
  82. package/web/dist/assets/{ActivityDetail-abt7hzD2.js → ActivityDetail-julsmPDB.js} +1 -1
  83. package/web/dist/assets/CatalogRow-DqS4e5XM.js +1 -0
  84. package/web/dist/assets/Diff-CIrGbiqm.js +5 -0
  85. package/web/dist/assets/Doctor-CpI53oHY.js +1 -0
  86. package/web/dist/assets/{Graph-DvYMtlvl.js → Graph-CS4tA7YS.js} +1 -1
  87. package/web/dist/assets/Harness-vRYs11kX.js +1 -0
  88. package/web/dist/assets/Laws-BjbHJrJt.js +3 -0
  89. package/web/dist/assets/Library-CjC7BAWN.js +1 -0
  90. package/web/dist/assets/{marked.esm-JKvyS0Pp.js → Markdown-kgpg9EQM.js} +15 -15
  91. package/web/dist/assets/Profile-CHr-PPf6.js +1 -0
  92. package/web/dist/assets/{Rules-CfsV1EVy.js → Rules-CsC6hfqk.js} +1 -1
  93. package/web/dist/assets/Skills-BvatJGfe.js +1 -0
  94. package/web/dist/assets/{StatusBadge-C9bZOEZk.js → StatusBadge-gRrak0nO.js} +1 -1
  95. package/web/dist/assets/{activity-Cc0n2jwx.js → activity-B0-FXZCj.js} +1 -1
  96. package/web/dist/assets/index-BnRWQbZb.js +12 -0
  97. package/web/dist/assets/index-Bx3I7cRW.js +2 -0
  98. package/web/dist/assets/index-D0f9tJlA.css +1 -0
  99. package/web/dist/assets/index-DJOhOwwR.js +1 -0
  100. package/web/dist/index.html +2 -2
  101. package/web/src/pages/Laws.jsx +42 -71
  102. package/web/dist/assets/Diff-D3_bVkih.js +0 -5
  103. package/web/dist/assets/Doctor-hXp7srsx.js +0 -1
  104. package/web/dist/assets/Harness-BIWei7N5.js +0 -1
  105. package/web/dist/assets/Laws-CxmnRdwt.js +0 -3
  106. package/web/dist/assets/Library-MjbOkHIW.js +0 -1
  107. package/web/dist/assets/Markdown-Dx_w0faR.js +0 -1
  108. package/web/dist/assets/Profile-BRFvv2gr.js +0 -1
  109. package/web/dist/assets/Skills-DArA6IHS.js +0 -1
  110. package/web/dist/assets/index-Bx3oxZ1o.css +0 -1
  111. package/web/dist/assets/index-DC3-GlCY.js +0 -1
  112. package/web/dist/assets/index-DP035wPa.js +0 -12
  113. package/web/dist/assets/index-dHi-7M-F.js +0 -2
package/js/web/api.mjs CHANGED
@@ -1,55 +1,13 @@
1
1
  /**
2
- * The web console's read endpoints — P6b's `overview`, `themes`, `doctor`, `diff`,
3
- * `installs`, `excludes`, `setup` and `profile`, P6c's `catalog`, `item` and `wiki_item`,
4
- * and the `WebState` all eleven read from.
2
+ * The web console's read endpoints — `overview`, `themes`, `doctor`, `diff`, `installs`,
3
+ * `excludes`, `setup`, `profile`, `catalog`, `item` and `wiki_item` — and the `WebState`
4
+ * all eleven read from.
5
5
  *
6
- * WHAT WAS ACTUALLY NEW HERE, MEASURED BY READING `js/` RATHER THAN BY GREPPING A NAME.
7
- * The P6 handoff scored this group at "~430 LOC new against ~1,600 already ported" and
8
- * the order was right, but the shape is worth stating because it is the same discovery
9
- * every P5 sub-phase made: six of the eight are a JSON face over a function `js/` already
10
- * owns. `apiSetup` is `statusData()` plus four fields. `apiDiff` is `diffCollect()`
11
- * reshaped. `apiExcludes` is `excludesSnapshot()`, unmodified. `apiInstalls` is a row per
12
- * `installTargets()` entry through five detectors P5d and P5f ported. `apiDoctor` is
13
- * `doctorCollect()` — with the one parameter it did not have. Only `apiThemes` and
14
- * `apiOverview` compute anything of their own, and most of what `apiOverview` computes is
15
- * counting.
16
- *
17
- * THE ONE PARAMETER: `doctorCollect({groups})`. `js/inspect/doctor.mjs` carried a `ran(check,
18
- * label, probs)` that returned its third argument and threw the first two away, with a
19
- * docblock saying the label stays "because it is the one place each check is NAMED, and
20
- * what P6's accumulator will key on". This is that phase. `on_progress=` — the TUI's — is
21
- * still absent, still P7's: nothing here passes it and adding it would be a claim no cell
22
- * can check.
23
- *
24
- * THE INVENTORY IS THE FULL RECORD SINCE P6c, AND THERE IS STILL ONE CLASSIFIER. P6b
25
- * shipped `inventoryFor` as three integers, because `apiOverview` reads three `len()`s off
26
- * `state.inventory` and the badges and taxonomy classes belonged to a phase that had not
27
- * arrived. `api_catalog` consumes exactly those fields, so this is the phase that would
28
- * have made the counting half and the reading half two classifiers. It did not:
29
- * `tuiInventory` in `js/inspect/inventory.mjs` is the one walk, `js/inspect/status.mjs`'s `inventoryCounts`
30
- * is three `length`s off it, and `specEntries` grew out of `specNames` by adding the READ
31
- * rather than by being written beside it.
32
- *
33
- * THE TAXONOMY WAS ALREADY PORTED, which the measurement found and the handoff did not.
34
- * `js/inspect/status.mjs` said flatly that "the ~111 LOC of TUI taxonomy" was P7's; `LAW_CLASS`,
35
- * `SKILL_CLASS`, `LAW_CLASSES` and `ENTITY_STATUSES` had in fact crossed in P5g inside
36
- * `js/inspect/checks-authoring.mjs`, because doctor's authoring gates are what validate them. P6c moved the
37
- * four to `js/inspect/inventory.mjs` — where Python keeps them — rather than copying them, and
38
- * doctor imports them back. Only `parse_laws`, `load_registry` and `entity_status` were
39
- * genuinely new.
40
- *
41
- * THE SNAPSHOT REPORTS NO INTERPRETER VERSION, and the absence is deliberate. The server
42
- * this one replaced ran on an interpreter and published its version under a key named for
43
- * it; nothing here answers that question, and answering it with this runtime's version
44
- * under that name would have been a lie the UI printed. So the field is gone rather than
45
- * present-and-empty. The recorded web corpus was taken from the old server and still
46
- * carries the key, which is why its replay harness erases it before comparing.
47
- *
48
- * P6c's ROUTES ARE THE FIRST THAT PARSE A PATH, which brings three things no earlier
49
- * endpoint could reach: `percentDecode` (NOT `decodeURIComponent`, which throws where the
50
- * reference leaves a stray `%` alone), the `NotFound` → 404 convention, and `flatName`'s
51
- * traversal refusal — a GET carries no token, so before that check the item route was an
52
- * arbitrary-file read.
6
+ * `PREFIX_ROUTES` below are the first routes that parse a path, which brings three things
7
+ * no state-keyed route needs: `percentDecode` (NOT `decodeURIComponent`, which throws on a
8
+ * `%` that is not an escape, where this shell instead answers a 404 naming the literal
9
+ * text), the `NotFound` → 404 convention, and `flatName`'s traversal refusal — a GET
10
+ * carries no token, so before that check the item route was an arbitrary-file read.
53
11
  */
54
12
  import { createHash } from 'node:crypto';
55
13
  import { existsSync, readdirSync, statSync } from 'node:fs';
@@ -73,7 +31,7 @@ import {
73
31
  import { firstBlockquote } from '../hosts/native.mjs';
74
32
  import { EMIT_OPTIONS, themeOptions } from '../maintain/setup.mjs';
75
33
  import { accentFor, statusData } from '../inspect/status.mjs';
76
- import { readText, withDiscardableStderr } from '../lib/fs.mjs';
34
+ import { readText, withDiscardableStderr, isDir, isFile } from '../lib/fs.mjs';
77
35
  import { comparePaths, normcase, within } from '../lib/paths.mjs';
78
36
  import { stripWhitespace, percentDecode } from '../lib/text.mjs';
79
37
  import { readJsonc } from '../hosts/settings.mjs';
@@ -81,27 +39,23 @@ import { apiActivity, apiActivityDetail } from './activity.mjs';
81
39
  import { apiMcp, apiRules } from './actions.mjs';
82
40
  import { apiGraph } from './graph.mjs';
83
41
 
84
- /** `_web_core.NotFound` — a requested catalog section or item that does not exist. */
42
+ /** A requested catalog section or item that does not exist. */
85
43
  export class NotFound extends Error {}
86
44
 
87
- const isDir = (p) => { try { return statSync(p).isDirectory(); } catch { return false; } };
88
- const isFile = (p) => { try { return statSync(p).isFile(); } catch { return false; } };
89
-
90
- /** Two resolved paths, compared as `PurePath.__eq__` does — case-folded on Windows. */
45
+ /** Two resolved paths, compared case-folded on Windows. */
91
46
  const samePath = (a, b) => normcase(resolvePath(a)) === normcase(resolvePath(b));
92
47
 
93
- /** `dict.get(key, default)` — `??` would also swallow an explicit null the file declared. */
48
+ /** `??` would also swallow an explicit `null` the file declared; this does not. */
94
49
  const dget = (obj, key, dflt) => (Object.hasOwn(obj, key) ? obj[key] : dflt);
95
50
 
96
- /** `hashlib.sha256(text.encode()).hexdigest()[:16]`, and "" for an empty file. */
51
+ /** SHA-256 hex, first 16 chars — `""` for an empty file. */
97
52
  export function fingerprint(text) {
98
53
  return text ? createHash('sha256').update(text, 'utf-8').digest('hex').slice(0, 16) : '';
99
54
  }
100
55
 
101
56
  /**
102
- * `datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M")` — LOCAL time, as the reference's
103
- * naive `fromtimestamp` is. `toISOString` would be UTC and would read as a port bug in
104
- * every timezone but one.
57
+ * `YYYY-MM-DD HH:MM`, LOCAL time — `toISOString` would be UTC, which reads wrong in every
58
+ * timezone but one for a user-facing timestamp.
105
59
  */
106
60
  export function stampMinute(ms) {
107
61
  const d = new Date(ms);
@@ -113,19 +67,19 @@ export function stampMinute(ms) {
113
67
  // ---- WebState ---------------------------------------------------------------------------
114
68
 
115
69
  /**
116
- * `_web_core.WebState` — the resolved view of the deployed harness a request reads from.
70
+ * The resolved view of the deployed harness a request reads from.
117
71
  *
118
72
  * A factory returning a plain object rather than a class, because the two cached
119
73
  * properties are the only behaviour and a getter pair expresses them without ceremony.
120
74
  * THE CACHING IS BEHAVIOUR, NOT AN OPTIMISATION: `doctorCollect` renders a theme per call
121
75
  * and the dashboard GETs `/api/overview` on every navigation, so an uncached `doctor`
122
76
  * would put a full build on the request path. `refresh()` is what a mutation calls to
123
- * drop both (P6f's).
77
+ * drop both.
124
78
  */
125
79
  export function webState(theme = null, target = null) {
126
80
  const st = {
127
- // `Path(target) if target else _opencode_config_dir()` — NOT resolved. `target` is
128
- // printed by three endpoints, so resolving it here would change what they answer.
81
+ // NOT resolved: `target` is printed by three endpoints, so resolving it here would
82
+ // change what they answer.
129
83
  target: target || opencodeConfigDir(),
130
84
  root: null,
131
85
  theme: null,
@@ -164,14 +118,12 @@ export function webState(theme = null, target = null) {
164
118
  st._doctor = { ok: !problems.length, problems, checked_at: stampMinute(Date.now()) };
165
119
  };
166
120
  /**
167
- * `WebState._detect_emit` — the CURRENT install's mode, from its `.geneseed-emit` marker:
168
- * the ROOT first (where every emit writes it), then the data dir.
121
+ * The CURRENT install's mode, from its `.geneseed-emit` marker: the ROOT first (where
122
+ * every emit writes it), then the data dir.
169
123
  *
170
- * P6b ported `refresh()` WITHOUT this, because nothing called `refresh()` yet and the
171
- * constructor reads `installedDefaults()` instead. P6f is the phase with mutations, so
172
- * `refresh()` finally runs — and the reference's `self.emit = self._detect_emit() or
173
- * self.emit` line was missing here, which would have left `emit` stale after any write
174
- * that re-themed or re-pointed the install. Restored with its caller.
124
+ * `refresh()` below MUST call this: without it, `emit` goes stale after any write that
125
+ * re-themes or re-points the install, since the constructor only reads it once via
126
+ * `installedDefaults()`.
175
127
  */
176
128
  st.detectEmit = () => {
177
129
  for (const d of [st.root, st.target]) {
@@ -181,12 +133,12 @@ export function webState(theme = null, target = null) {
181
133
  const v = stripWhitespace(readText(em));
182
134
  if (v) return v;
183
135
  }
184
- } catch { /* as the Python's `except OSError: pass` */ }
136
+ } catch { /* unreadable marker: try the next candidate */ }
185
137
  }
186
138
  return existsSync(path.join(st.root, 'CLAUDE.md')) ? 'claude-global' : 'opencode-global';
187
139
  };
188
140
  /**
189
- * `WebState.select_view` — re-point the console at another detected install's data dir.
141
+ * Re-point the console at another detected install's data dir.
190
142
  *
191
143
  * `root` is the install ROOT the markers and sigils live at. It defaults to `target` and
192
144
  * differs only for claude/bob/copilot PROJECT installs, where the data sits under
@@ -217,13 +169,12 @@ export function webState(theme = null, target = null) {
217
169
  return st;
218
170
  }
219
171
 
220
- /** `_web_core._deployed`. */
221
172
  export function deployed(state) {
222
173
  return existsSync(path.join(state.target, GLOBAL_MANIFEST));
223
174
  }
224
175
 
225
176
  /**
226
- * `_web_core._spec_desc` — a deployed spec's one-line purpose.
177
+ * A deployed spec's one-line purpose.
227
178
  *
228
179
  * The `> blockquote` convention every rendered skill and agent carries, then the
229
180
  * frontmatter `description`, then the first prose paragraph. The fallbacks are for the
@@ -243,13 +194,13 @@ export function specDesc(fm, body) {
243
194
  }
244
195
 
245
196
  /**
246
- * `_web_core._spec_entries` — agent/skill specs read straight off a DEPLOYED harness dir.
197
+ * Agent/skill specs read straight off a DEPLOYED harness dir.
247
198
  *
248
199
  * Agents are flat `<root>/<name>.md` (skipping `_*` templates); skills use OpenCode's
249
- * folder layout `<root>/<name>/SKILL.md`. `glob("*.md")` is case-INSENSITIVE on Windows,
250
- * as pathlib's is — `normcase` rather than a bare `endsWith`. The frontmatter is stripped
251
- * because it is host plumbing rather than prose, which is what makes a deployed entry the
252
- * same shape as a source-rendered one and every consumer indifferent to the origin.
200
+ * folder layout `<root>/<name>/SKILL.md`. Matched case-INSENSITIVE on Windows via
201
+ * `normcase`, not a bare `endsWith`. The frontmatter is stripped because it is host
202
+ * plumbing rather than prose, which is what makes a deployed entry the same shape as a
203
+ * source-rendered one and every consumer indifferent to the origin.
253
204
  */
254
205
  export function specEntries(root, nested) {
255
206
  const out = [];
@@ -272,8 +223,7 @@ export function specEntries(root, nested) {
272
223
  }
273
224
 
274
225
  /**
275
- * `_web_core._deployed_inventory` — the agents and skills actually installed at
276
- * `state.target`, not a fresh render of `src/`.
226
+ * The agents and skills actually installed at `state.target`, not a fresh render of `src/`.
277
227
  *
278
228
  * All three constitutional tiers still come from the render: once deployed they live inside
279
229
  * AGENT.md rather than as separate files, so the deployed arm replaces the two ENTITY rosters
@@ -309,28 +259,25 @@ export function deployedInventory(state) {
309
259
  theme: state.theme };
310
260
  }
311
261
 
312
- /** `WebState.inventory` — the deployed record when there is one, else the source render. */
262
+ /** The deployed record when there is one, else the source render. */
313
263
  export function inventoryFor(state) {
314
264
  return deployed(state) ? deployedInventory(state) : tuiInventory(state.theme);
315
265
  }
316
266
 
317
267
  // ---- the catalog stores overview counts ---------------------------------------------------
318
268
 
319
- /** `_web_catalog._memory_dir` — always `<target>/memory`, never the CWD-scanning resolver. */
269
+ /** Always `<target>/memory` — never the CWD-scanning resolver. */
320
270
  const memoryDir = (state) => path.join(state.target, 'memory');
321
271
  const notebookDir = (state) => path.join(state.target, 'notebook');
322
272
 
323
273
  /**
324
- * `sorted(pathlib.Path.glob("*.md"))` — matched, case-insensitive on Windows.
274
+ * Matched case-insensitively on Windows.
325
275
  *
326
- * `comparePaths` AND NOT A BARE `.sort()`, and this was a LIVE BUG until P6f's memory cells
327
- * put an uppercase filename in the directory. `sorted()` over `Path` objects compares
328
- * `_str_normcase`, so on Windows `MEMORY.md` sorts under `m` and lands after `a-fact.md`;
329
- * JS's default comparator is UTF-16 code units, so `M` (0x4D) sorts before `a` (0x61) and
330
- * the whole catalog came back in a different order. Every seeded memory and notebook file in
331
- * P6b and P6c was lower-case, which is why nothing said so for three phases. The `normcase`
332
- * in the filter beside it was already here for exactly this reason — the sort was the half
333
- * that got missed.
276
+ * `comparePaths` and NOT a bare `.sort()`: on Windows, filenames sort case-insensitively, so
277
+ * `MEMORY.md` sorts under `m` and lands after `a-fact.md`; JS's default comparator is UTF-16
278
+ * code units, so `M` (0x4D) sorts before `a` (0x61) and the whole catalog comes back in a
279
+ * different order. The `normcase` in the filter beside it only fixes matching, not
280
+ * ordering — this is the other half.
334
281
  */
335
282
  function globMd(dir) {
336
283
  if (!isDir(dir)) return [];
@@ -340,9 +287,9 @@ function globMd(dir) {
340
287
  .sort(comparePaths);
341
288
  }
342
289
 
343
- const stemOf = (name) => name.slice(0, name.length - path.extname(name).length);
290
+ /** Filename without its extension — single owner, imported by `js/web/activity.mjs` too. */
291
+ export const stemOf = (name) => name.slice(0, name.length - path.extname(name).length);
344
292
 
345
- /** `_web_catalog._memory_items`. */
346
293
  export function memoryItems(state) {
347
294
  const d = memoryDir(state);
348
295
  if (!isDir(d)) return [];
@@ -359,7 +306,6 @@ export function memoryItems(state) {
359
306
  });
360
307
  }
361
308
 
362
- /** `_web_catalog._notebook_items`. */
363
309
  export function notebookItems(state) {
364
310
  const d = notebookDir(state);
365
311
  if (!isDir(d)) return [];
@@ -368,13 +314,12 @@ export function notebookItems(state) {
368
314
  }));
369
315
  }
370
316
 
371
- /** `_web_catalog._CONFIG_META` — the two setup manifests, in listing order. */
317
+ /** The two setup manifests, in listing order. */
372
318
  const CONFIG_META = {
373
319
  'context.json': ['Project context', 'what the agent loads for this project'],
374
320
  'wiki.jsonc': ['Wiki manifest', 'your machine-wide knowledge base(s)'],
375
321
  };
376
322
 
377
- /** `_web_catalog._config_items`. */
378
323
  export function configItems(state) {
379
324
  const out = [];
380
325
  for (const fname of ['context.json', 'wiki.jsonc']) {
@@ -395,23 +340,18 @@ export const WIKI_FILE_CAP = 5000;
395
340
  * The three wiki sites below, and `apiDeployCmd` in `js/web/actions.mjs`, are the
396
341
  * request-path callers of `expanduser`, and it REFUSES a `~user` form by printing and throwing
397
342
  * (`js/hosts/hosts.mjs`'s docblock). None of them may let that escape: `js/web/handler.mjs`
398
- * wraps the whole GET in a blanket `catch` → a JSON
399
- * 500, so an unguarded refusal would turn ONE bad line in a file the user hand-edits into a
400
- * dead Knowledge section. That is the same argument `sovereignBypass` makes in `js/hosts/hooks.mjs`
401
- * — contain the refusal per entry and take the site's OWN degrade — and each of the three
402
- * already has one: `[]`, `continue`, `continue`-into-404. The reference reaches those same
403
- * three degrades for a `~user` path, from the other direction: `ntpath.expanduser` expands
404
- * `~nosuchuser` to `C:\Users\nosuchuser\…`, which does not exist, so `is_file()`/`is_dir()`
405
- * answer False. `_wiki_path` in `_web_catalog.py` is the guard that makes POSIX agree, where
406
- * the same call raises RuntimeError instead.
343
+ * wraps the whole GET in a blanket `catch` → a JSON 500, so an unguarded refusal would turn
344
+ * ONE bad line in a file the user hand-edits into a dead Knowledge section. That is the same
345
+ * argument `sovereignBypass` makes in `js/hosts/hooks.mjs` — contain the refusal per entry
346
+ * and take the site's OWN degrade — and each of the three already has one: `[]`, `continue`,
347
+ * `continue`-into-404.
407
348
  *
408
- * `withDiscardableStderr` because `expanduser` writes its refusal at the RAISE SITE and this
409
- * caller catches — the reference prints nothing here, and `<server stderr>` is compared as
410
- * bytes by `tests/web_golden.py`, so replaying it would be a divergence on every cell.
349
+ * `withDiscardableStderr` because `expanduser` prints its refusal at the RAISE SITE, and
350
+ * this caller wants that swallowed along with the exception rather than logged.
411
351
  *
412
- * DELIBERATE, ADJUDICATED RESIDUE: if `C:\Users\<other-user>\…` really exists, the reference
413
- * on Windows READS ANOTHER ACCOUNT'S FILE where this refuses. Refusing is the settled product
414
- * decision — the reference is the side that is wrong — and the reference is being deleted.
352
+ * DELIBERATE PRODUCT DECISION: a `~otherUser` path is refused rather than resolved, even
353
+ * where the account happens to exist — reading another account's files through a
354
+ * hand-edited wiki manifest is not a feature.
415
355
  */
416
356
  function wikiPath(p) {
417
357
  try {
@@ -422,12 +362,12 @@ function wikiPath(p) {
422
362
  }
423
363
 
424
364
  /**
425
- * `_web_catalog._wiki_manifest` — `$GENESEED_WIKI` first, else `wiki.jsonc` beside the
426
- * deployed bundle, read with the harness's generic JSONC loader.
365
+ * `$GENESEED_WIKI` first, else `wiki.jsonc` beside the deployed bundle, read with the
366
+ * harness's generic JSONC loader.
427
367
  *
428
- * `resolvePath` where the reference only `expanduser()`s. NOT OBSERVABLE, so it stays: `p` is
429
- * consumed by `isFile` and `mcpLoad` and never reaches a response body, and both of those
430
- * follow symlinks and resolve a relative path against the same cwd anyway.
368
+ * `resolvePath`, not merely `expanduser`: harmless here because `p` is consumed by `isFile`
369
+ * and `mcpLoad` and never reaches a response body, and both of those follow symlinks and
370
+ * resolve a relative path against the same cwd anyway.
431
371
  */
432
372
  function wikiManifest(state) {
433
373
  const cand = process.env.GENESEED_WIKI;
@@ -439,16 +379,14 @@ function wikiManifest(state) {
439
379
  }
440
380
 
441
381
  /**
442
- * `harness._mcp_load(path)` with no host — the COMMENT-TOLERANT dict loader, `{}` for a
443
- * file that is missing, unreadable, unparseable, or not an object.
382
+ * The COMMENT-TOLERANT dict loader, `{}` for a file that is missing, unreadable,
383
+ * unparseable, or not an object.
444
384
  *
445
- * Comment-tolerant is the whole point and the first draft of this got it wrong. The two
446
- * files it reads are `wiki.jsonc` and `context.json`, and the first is `.jsonc` because it
447
- * is HAND-MAINTAINED: a `//` in it parses on the reference and would have made
448
- * `JSON.parse` return null here, so the wiki section would have silently listed nothing
449
- * and the config item's `manifest` would have come back empty. The seeded manifests in
450
- * `tests/web_golden.py` are plain JSON, so no cell had one — `catalog/…-tolerates-comments`
451
- * is the cell that does now.
385
+ * Comment-tolerant is the whole point, and getting this wrong is silent: the two files it
386
+ * reads are `wiki.jsonc` and `context.json`, hand-maintained, so a `//` line is expected.
387
+ * Plain `JSON.parse` would throw on it and this loader would then answer `{}` — the wiki
388
+ * section listing nothing and the config item's `manifest` coming back empty, with no
389
+ * error surfaced anywhere.
452
390
  */
453
391
  function mcpLoad(p) {
454
392
  if (!isFile(p)) return {};
@@ -458,29 +396,28 @@ function mcpLoad(p) {
458
396
  return data && typeof data === 'object' && !Array.isArray(data) ? data : {};
459
397
  }
460
398
 
461
- /** Every `.md` under `dir`, recursively, POSIX-relative to it and sorted — `rglob("*.md")`. */
462
- function rglobMd(dir, base = dir, out = []) {
399
+ /**
400
+ * Every `.md` under `dir`, recursively.
401
+ *
402
+ * `recursive: true` replaces a hand-rolled walk; its one caller re-sorts the result with
403
+ * `comparePaths` before using it, so the order this returns is never observed.
404
+ */
405
+ function rglobMd(dir) {
463
406
  let entries;
464
- try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return out; }
465
- for (const e of entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))) {
466
- const full = path.join(dir, e.name);
467
- if (e.isDirectory()) rglobMd(full, base, out);
468
- else if (normcase(e.name).endsWith('.md')) out.push(full);
469
- }
470
- return out;
407
+ try { entries = readdirSync(dir, { recursive: true, withFileTypes: true }); } catch { return []; }
408
+ return entries
409
+ .filter((e) => e.isFile() && normcase(e.name).endsWith('.md'))
410
+ .map((e) => path.join(e.parentPath, e.name));
471
411
  }
472
412
 
473
- /** `_web_catalog._wiki_items`. */
474
413
  export function wikiItems(state) {
475
414
  const items = [];
476
415
  const seen = new Set();
477
416
  for (const w of wikiManifest(state)) {
478
417
  if (!w || typeof w !== 'object' || Array.isArray(w)) continue;
479
418
  const wname = String(dget(w, 'name', null) || 'wiki');
480
- // PER ENTRY, and the loop continues — one unusable `path` in the manifest must not blank
481
- // every OTHER vault in it. `resolvePath` where the reference only `expanduser()`s is again
482
- // unobservable: `root` is only ever joined against or used as the base of a `relative()`
483
- // whose other operand was built FROM it, and `source` is `.resolve()`d on both sides.
419
+ // PER ENTRY, and the loop continues — one unusable `path` in the manifest must not
420
+ // blank every OTHER vault in it.
484
421
  const root = wikiPath(String(dget(w, 'path', null) || ''));
485
422
  if (!root || !isDir(root)) continue;
486
423
  const entries = (dget(w, 'entries', null) || [])
@@ -496,7 +433,7 @@ export function wikiItems(state) {
496
433
  const fp = path.join(root, rel);
497
434
  let mds;
498
435
  if (isFile(fp) && path.extname(fp) === '.md') mds = [fp];
499
- // `sorted(fp.rglob("*.md"))[:cap]` — `comparePaths`, for the reason `globMd` carries.
436
+ // Capped and sorted via `comparePaths`, for the reason `globMd` carries.
500
437
  else if (isDir(fp)) mds = rglobMd(fp).sort(comparePaths).slice(0, WIKI_FILE_CAP);
501
438
  else continue;
502
439
  for (const md of mds) {
@@ -513,7 +450,7 @@ export function wikiItems(state) {
513
450
  return items;
514
451
  }
515
452
 
516
- /** `_web_catalog.api_wiki_item` — one page by `<wiki>:<relpath>`, never outside the vault. */
453
+ /** One page by `<wiki>:<relpath>`, never outside the vault. */
517
454
  export function apiWikiItem(state, name) {
518
455
  const at = name.indexOf(':');
519
456
  const wname = at < 0 ? name : name.slice(0, at);
@@ -522,9 +459,8 @@ export function apiWikiItem(state, name) {
522
459
  for (const w of wikiManifest(state)) {
523
460
  if (!w || typeof w !== 'object' || Array.isArray(w)) continue;
524
461
  if (String(dget(w, 'name', null) || 'wiki') !== wname) continue;
525
- // The reference is `.expanduser().resolve()` here, so this site matches on resolve and
526
- // differs only on the refusal — which falls through to the `NotFound` below, exactly
527
- // where the reference lands when the expanded root turns out not to exist.
462
+ // An unresolvable or refused root falls through to the `NotFound` below, the same as
463
+ // one that simply does not exist.
528
464
  const root = wikiPath(String(dget(w, 'path', null) || ''));
529
465
  if (!root) continue;
530
466
  const p = resolvePath(path.join(root, rel));
@@ -538,19 +474,18 @@ export function apiWikiItem(state, name) {
538
474
  }
539
475
 
540
476
  /**
541
- * `harness._within` — re-exported from `js/lib/paths.mjs`, where the lib split put it.
477
+ * Re-exported from `js/lib/paths.mjs`, where the lib split put it.
542
478
  *
543
479
  * ⚠ A SECOND, INDEPENDENT COPY lives in `js/inspect/scan.mjs`, written for the doctor's tree
544
480
  * walks. A change to the containment rule has to land on both, and nothing compares them.
545
481
  *
546
- * It lived here from P6b (the restore's containment check) until `_install_move_list` needed
547
- * the same predicate from `js/maintain/uninstall.mjs`, which must not import the web tree. The
548
- * re-export keeps `js/web/actions.mjs`'s import path unchanged.
482
+ * Re-exported here (rather than imported directly by callers) so `js/web/actions.mjs`'s
483
+ * import path stays unchanged.
549
484
  */
550
485
  export { within };
551
486
 
552
487
  /**
553
- * `_web_catalog._flat_name` — catalog names are flat basenames.
488
+ * Catalog names are flat basenames.
554
489
  *
555
490
  * A separator, a `..` or a drive colon in the URL segment is someone steering the join
556
491
  * outside the catalog dir. A GET carries no token, so before this check the endpoint was
@@ -563,16 +498,14 @@ function flatName(name) {
563
498
  }
564
499
  }
565
500
 
566
- /** `_web_core.WIKILINK_RE`. Exported since P6e — `js/web/graph.mjs` is its second reader. */
501
+ /** `js/web/graph.mjs` is a second reader of this pattern. */
567
502
  export const WIKILINK_RE = /\[\[([^\]]+)\]\]/g;
568
503
 
569
504
  /**
570
- * `_web_catalog._resolve_links` — `[[name]]` matched against known agent and skill names.
505
+ * `[[name]]` matched against known agent and skill names.
571
506
  *
572
507
  * AGENTS FIRST, SKILLS SECOND, into ONE map — so a name that is both resolves as a SKILL.
573
- * That is not a preference, it is what the reference's two loops do in that order, and
574
- * `item/an-agent-carries-its-body-and-its-resolved-links` is the cell that would catch the
575
- * other order.
508
+ * This ordering is deliberate and must not be swapped.
576
509
  */
577
510
  export function resolveLinks(state, body) {
578
511
  const inv = state.inventory;
@@ -582,8 +515,8 @@ export function resolveLinks(state, body) {
582
515
  const links = [];
583
516
  const seen = new Set();
584
517
  for (const m of body.matchAll(WIKILINK_RE)) {
585
- // `stripWhitespace`, not `trim()` — P6d measured the two whitespace classes apart and P6e
586
- // gave this regex a second reader, so the two call sites answer with one rule.
518
+ // `stripWhitespace`, not `trim()` — the two whitespace classes differ, and both call
519
+ // sites on this regex must answer with the same rule.
587
520
  const label = stripWhitespace(m[1]);
588
521
  if (known.has(label) && !seen.has(label)) {
589
522
  seen.add(label);
@@ -594,8 +527,8 @@ export function resolveLinks(state, body) {
594
527
  }
595
528
 
596
529
  /**
597
- * `_web_catalog._config_manifest` — a setup file parsed into the shape the detail pane
598
- * renders as cards, or `null` so the caller falls back to the raw body.
530
+ * A setup file parsed into the shape the detail pane renders as cards, or `null` so the
531
+ * caller falls back to the raw body.
599
532
  */
600
533
  function configManifest(name, p) {
601
534
  const cfg = mcpLoad(p);
@@ -611,7 +544,7 @@ function configManifest(name, p) {
611
544
  }
612
545
 
613
546
  /**
614
- * `_web_core.SECTIONS` — a closed list; anything else is a 404.
547
+ * A closed list; anything else is a 404.
615
548
  *
616
549
  * ⚠ `laws` STAYS ONE SECTION FOR ALL THREE TIERS, and that is a decision rather than an
617
550
  * oversight. The console has ONE Constitution entry, not three, so a reader keeps the "read it
@@ -659,7 +592,6 @@ function constitutionItems(inv) {
659
592
  ];
660
593
  }
661
594
 
662
- /** `_web_catalog.api_catalog`. */
663
595
  export function apiCatalog(state, section) {
664
596
  if (!SECTIONS.includes(section)) throw new NotFound(section);
665
597
  const inv = state.inventory;
@@ -684,7 +616,6 @@ export function apiCatalog(state, section) {
684
616
  return { section, items };
685
617
  }
686
618
 
687
- /** `_web_catalog.api_item`. */
688
619
  export function apiItem(state, type, name) {
689
620
  const inv = state.inventory;
690
621
  if (type === 'agent' || type === 'skill') {
@@ -737,7 +668,7 @@ export function apiItem(state, type, name) {
737
668
 
738
669
  // ---- the eight endpoints -------------------------------------------------------------
739
670
 
740
- /** `_web_actions._theme_choices`. Exported since P6g — `_build_override` is its third reader. */
671
+ /** `buildOverride` in `actions.mjs` is a third reader of this. */
741
672
  export function themeChoices() {
742
673
  return themeOptions().map(([name, blurb]) => {
743
674
  const data = readJsonMaybe(path.join(THEMES, `${name}.json`));
@@ -747,16 +678,19 @@ export function themeChoices() {
747
678
  });
748
679
  }
749
680
 
750
- /** `_web_actions._emit_choices`. */
751
681
  export const emitChoices = () => EMIT_OPTIONS.map(([name, desc]) => ({ name, desc }));
752
682
 
753
- /** `_web_actions.api_themes`. */
754
683
  export function apiThemes(state) {
755
684
  return { themes: themeChoices(), emits: emitChoices(),
756
685
  current: { theme: state.theme, emit: state.emit } };
757
686
  }
758
687
 
759
- /** `_web_actions.api_setup`. */
688
+ /**
689
+ * No interpreter/runtime version field, deliberately: this server has no interpreter
690
+ * version worth reporting under that name, and reporting this runtime's own version there
691
+ * would misrepresent what actually changed. The field is absent rather than
692
+ * present-and-empty.
693
+ */
760
694
  export function apiSetup(state) {
761
695
  return {
762
696
  ...statusData(),
@@ -766,7 +700,7 @@ export function apiSetup(state) {
766
700
  };
767
701
  }
768
702
 
769
- /** `_web_actions.api_doctor` — the same engine as the `doctor` verb, grouped per check. */
703
+ /** The same engine as the `doctor` verb, grouped per check. */
770
704
  export function apiDoctor(state) {
771
705
  const groups = [];
772
706
  const [themes, problems] = doctorCollect({ theme: state.theme, groups });
@@ -775,19 +709,18 @@ export function apiDoctor(state) {
775
709
  checked_at: state.doctor.checked_at };
776
710
  }
777
711
 
778
- /** `_web_actions.api_diff`. */
779
712
  export function apiDiff(state) {
780
713
  const { target, theme, files } = diffCollect({
781
714
  target: state.target, theme: state.theme, emit: state.emit });
782
715
  return { deployed: files !== null, target, theme, files: files || [] };
783
716
  }
784
717
 
785
- /** `_web_actions.api_excludes` — the union across every global install, verbatim. */
718
+ /** The union across every global install, verbatim. */
786
719
  export const apiExcludes = () => excludesSnapshot();
787
720
 
788
721
  /**
789
- * `_web_actions._view_cfg` — the data dir an install's inventory/memory/diff is read from.
790
- * Host-driven, so a new nested-marker host cannot silently read the bare root.
722
+ * The data dir an install's inventory/memory/diff is read from. Host-driven, so a new
723
+ * nested-marker host cannot silently read the bare root.
791
724
  */
792
725
  export function viewCfg(host, scope, root) {
793
726
  if (scope === 'project' && ['claude', 'bob', 'copilot'].includes(host)) {
@@ -796,7 +729,6 @@ export function viewCfg(host, scope, root) {
796
729
  return root;
797
730
  }
798
731
 
799
- /** `_web_actions.api_installs`. */
800
732
  export function apiInstalls(state) {
801
733
  const out = [];
802
734
  for (const [host, scope, root] of installTargets()) {
@@ -814,11 +746,10 @@ export function apiInstalls(state) {
814
746
  modes: discoverNames('modes', 'direct') };
815
747
  }
816
748
 
817
- /** `_web_catalog._profile_path` — `build.PROFILE_FILE` beside the deployed AGENT.md. */
749
+ /** Beside the deployed AGENT.md. */
818
750
  export const PROFILE_FILE = 'PROFILE.md';
819
751
  export const profilePath = (state) => path.join(state.target, PROFILE_FILE);
820
752
 
821
- /** `_web_catalog.api_profile`. */
822
753
  export function apiProfile(state) {
823
754
  const p = profilePath(state);
824
755
  if (!isFile(p)) return { exists: false, path: p, text: '', fingerprint: '' };
@@ -826,7 +757,7 @@ export function apiProfile(state) {
826
757
  return { exists: true, path: p, text, fingerprint: fingerprint(text) };
827
758
  }
828
759
 
829
- /** `_web_overview.api_overview` — the dashboard aggregate. */
760
+ /** The dashboard aggregate. */
830
761
  export function apiOverview(state) {
831
762
  const inv = state.inventory;
832
763
  let diff = null;
@@ -855,8 +786,7 @@ export function apiOverview(state) {
855
786
  buildEpoch = Math.floor(ms / 1000);
856
787
  }
857
788
  // Which detected install the current view points at, so the dashboard's footprint hero
858
- // can re-emit exactly it. Mirrors `viewCfg`'s rule, spelled out as the reference spells
859
- // it out — the two are separate code in the Python too.
789
+ // can re-emit exactly it. Mirrors `viewCfg`'s rule, spelled out separately here.
860
790
  let install = null;
861
791
  for (const [host, scope, root] of installTargets()) {
862
792
  const data = (scope === 'project' && ['claude', 'bob', 'copilot'].includes(host))
@@ -866,7 +796,7 @@ export function apiOverview(state) {
866
796
  install = { host, scope, path: root, footprint: footprintOfDir(root) };
867
797
  break;
868
798
  }
869
- } catch { /* as the Python's `except OSError: pass` */ }
799
+ } catch { /* an unreadable candidate just isn't a match */ }
870
800
  }
871
801
  return {
872
802
  theme: state.theme,
@@ -944,9 +874,9 @@ const RECENT_LIMIT = 8;
944
874
  /**
945
875
  * THE NEWEST FILE-BACKED ENTRIES ACROSS THE HARNESS — the "freshly grown" card's whole payload.
946
876
  *
947
- * A GET BEYOND THE REFERENCE (`GET_BEYOND_REF` in `routes.mjs`), and the first one. It exists
948
- * because NOTHING else in this API carries a per-entry date: every catalog row is
949
- * `{name, title, desc, source}` and the only two timestamps on the whole surface are
877
+ * A GET added after the reference surface was frozen (`GET_BEYOND_REF` in `routes.mjs`), and
878
+ * the first one. It exists because NOTHING else in this API carries a per-entry date: every
879
+ * catalog row is `{name, title, desc, source}` and the only two timestamps on the whole surface are
950
880
  * `overview.build_time` and `doctor.checked_at`. A client cannot stat a path, so "what changed
951
881
  * lately" was unanswerable without a server that looks.
952
882
  *
@@ -1001,57 +931,36 @@ function releaseLabel() {
1001
931
  }
1002
932
 
1003
933
  /**
1004
- * `_web_server.Handler.STATE_ROUTES`, the ported half.
1005
- *
1006
- * A literal path to `api_X(state)`, exactly as the reference's table is — and the reason
1007
- * it is a table on this side too is `tests/unit/web_server.test.mjs`'s cross-check, which
1008
- * requires this table plus `PREFIX_ROUTES`, `PORTED_INLINE` and `NOT_PORTED` to equal
1009
- * `REF_GET` — a FROZEN LITERAL of what the reference daemon answered, since the source it
1010
- * was once read from with `ast` is deleted. On the second instance of anything, the gate
1011
- * becomes a table cross-checked against a declaration.
1012
- */
1013
- /**
1014
- * `_web_server.do_GET`'s prefix routes, the ported ones — path in, response out.
934
+ * The prefix routes — path in, response out.
1015
935
  *
1016
936
  * `percentDecode` and not `decodeURIComponent`: the JS builtin throws a `URIError` on a `%`
1017
- * that is not an escape, which the shell would answer as a 500 where the reference answers
1018
- * a 404 naming the literal text.
937
+ * that is not an escape, where this shell instead answers a 404 naming the literal text.
1019
938
  */
1020
939
  export const PREFIX_ROUTES = [
1021
940
  ['/api/catalog/', (state, p) => apiCatalog(state, p.split('/').pop())],
1022
- // `path.split("/", 4)` — /api/item/<type>/<name>, and the NAME keeps its slashes so a
1023
- // wiki page's relpath survives. A missing name is a 404 here rather than an IndexError
1024
- // and a 500 two frames down.
941
+ // /api/item/<type>/<name> — TYPE has no slash, the NAME keeps its slashes so a wiki
942
+ // page's relpath survives. A missing name is a 404 here rather than a 500 two frames down.
1025
943
  ['/api/item/', (state, p) => {
1026
- const parts = splitN(p, '/', 4);
1027
- if (parts.length < 5 || !parts[4]) throw new NotFound(p);
1028
- return apiItem(state, parts[3], percentDecode(parts[4]));
944
+ const m = /^\/api\/item\/([^/]+)\/(.+)$/.exec(p);
945
+ if (!m) throw new NotFound(p);
946
+ return apiItem(state, m[1], percentDecode(m[2]));
1029
947
  }],
1030
- // P6e. The reference's own `path[len("/api/activity/"):]`, unquoted — the sid keeps
1031
- // whatever it carries and `api_activity_detail`'s safe-name scheme is what makes the
1032
- // lookup safe, rather than a check out here.
948
+ // The sid is passed through unquoted — `apiActivityDetail`'s safe-name scheme is what
949
+ // makes the lookup safe, rather than a check out here.
1033
950
  ['/api/activity/', (state, p) => apiActivityDetail(
1034
951
  state, percentDecode(p.slice('/api/activity/'.length)))],
1035
952
  ];
1036
953
 
1037
- /** `str.split(sep, maxsplit)` — at most `n` splits, the remainder kept whole. */
1038
- function splitN(s, sep, n) {
1039
- const out = [];
1040
- let rest = s;
1041
- for (let i = 0; i < n; i += 1) {
1042
- const at = rest.indexOf(sep);
1043
- if (at < 0) break;
1044
- out.push(rest.slice(0, at));
1045
- rest = rest.slice(at + sep.length);
1046
- }
1047
- out.push(rest);
1048
- return out;
1049
- }
1050
-
954
+ /**
955
+ * A literal path to `api_X(state)`, table-keyed rather than a chain of `if`s — the reason
956
+ * it is a table is `tests/unit/web_server.test.mjs`'s cross-check, which requires this
957
+ * table plus `PREFIX_ROUTES`, `PORTED_INLINE` and `NOT_PORTED` to equal `REF_GET`, a frozen
958
+ * record of every GET this daemon has ever needed to answer.
959
+ */
1051
960
  export const STATE_ROUTES = {
1052
961
  '/api/overview': apiOverview,
1053
- // Beyond the reference — declared in `GET_BEYOND_REF`, which is what keeps the partition
1054
- // gate's set equality honest instead of forging `REF_GET`.
962
+ // Added after the reference surface was frozen — declared in `GET_BEYOND_REF`, which
963
+ // keeps the partition gate's set equality honest instead of forging `REF_GET`.
1055
964
  '/api/recent': apiRecent,
1056
965
  '/api/themes': apiThemes,
1057
966
  '/api/setup': apiSetup,
@@ -1062,9 +971,9 @@ export const STATE_ROUTES = {
1062
971
  '/api/diff': apiDiff,
1063
972
  '/api/graph': apiGraph,
1064
973
  '/api/activity': apiActivity,
1065
- // P6f. `/api/rules` had to cross as a PAIR: `api_rules_mutate` splices `parse_rules`' line
1066
- // indices, and the fingerprint every mutation must send back is what this GET answers —
1067
- // so a Node daemon whose POST worked and whose GET 501'd would have no way to obtain one.
974
+ // `/api/rules` must exist as a PAIR with its POST: `apiRulesMutate` splices `parseRules`'
975
+ // line indices, and the fingerprint every mutation must send back is what this GET
976
+ // answers — a daemon whose POST worked and whose GET 501'd would have no way to obtain one.
1068
977
  '/api/rules': apiRules,
1069
978
  '/api/mcp': apiMcp,
1070
979
  };