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.
- package/CHANGELOG.md +70 -0
- package/README.md +2 -2
- package/SHIPPED.md +3 -3
- package/bin/build-driver.mjs +127 -79
- package/bin/geneseed-cli.mjs +42 -84
- package/bin/geneseed-hook.mjs +5 -2
- package/harness.config.json +1 -1
- package/js/README.md +1 -10
- package/js/build/emit-claude.mjs +4 -5
- package/js/build/emit-common.mjs +63 -114
- package/js/build/emit-opencode.mjs +74 -66
- package/js/build/generate.mjs +4 -6
- package/js/build/render.mjs +13 -13
- package/js/build/source.mjs +7 -6
- package/js/build/stubs.mjs +25 -33
- package/js/build/themes.mjs +1 -1
- package/js/build/version.mjs +1 -3
- package/js/hosts/hooks.mjs +3 -3
- package/js/hosts/hosts.mjs +2 -4
- package/js/hosts/installs.mjs +35 -28
- package/js/hosts/link.mjs +1 -1
- package/js/hosts/native.mjs +37 -19
- package/js/hosts/opencode.mjs +39 -28
- package/js/hosts/settings.mjs +33 -110
- package/js/inspect/checks-authoring.mjs +5 -4
- package/js/inspect/checks-build.mjs +1 -1
- package/js/inspect/diff.mjs +1 -3
- package/js/inspect/doctor.mjs +59 -44
- package/js/inspect/excludes.mjs +4 -10
- package/js/inspect/inventory.mjs +37 -55
- package/js/inspect/registry.mjs +4 -7
- package/js/inspect/scan.mjs +8 -15
- package/js/inspect/status.mjs +1 -1
- package/js/lib/fs.mjs +34 -0
- package/js/lib/json.mjs +28 -1
- package/js/lib/paths.mjs +0 -10
- package/js/lib/udiff.mjs +11 -16
- package/js/maintain/memory.mjs +2 -4
- package/js/maintain/migrate.mjs +3 -3
- package/js/maintain/uninstall.mjs +90 -78
- package/js/maintain/update.mjs +2 -2
- package/js/ui/anim.mjs +1 -1
- package/js/ui/menu.mjs +1 -1
- package/js/ui/tui.mjs +3 -20
- package/js/web/actions.mjs +230 -217
- package/js/web/activity.mjs +71 -89
- package/js/web/api.mjs +133 -224
- package/js/web/daemon.mjs +92 -110
- package/js/web/docs.mjs +66 -111
- package/js/web/graph.mjs +0 -0
- package/js/web/handler.mjs +78 -97
- package/js/web/jobs.mjs +124 -165
- package/js/web/routes.mjs +73 -152
- package/js/web/server.mjs +47 -82
- package/package.json +1 -1
- package/src/agents/explorer.md +2 -1
- package/src/doctrines/process.md +4 -2
- package/src/laws/universal.md +55 -43
- package/src/skills/clarify.md +1 -1
- package/src/skills/forge-mcp.md +2 -1
- package/src/skills/fresh-eyes.md +1 -1
- package/src/skills/ingest.md +2 -1
- package/src/skills/parallel-agents.md +1 -1
- package/src/skills/ponytail.md +2 -2
- package/src/skills/research.md +4 -2
- package/themes/_TEMPLATE.json +2 -0
- package/themes/biker.json +2 -0
- package/themes/commentator.json +2 -0
- package/themes/cyberpunk.json +2 -0
- package/themes/gamer.json +2 -0
- package/themes/imperial.json +2 -0
- package/themes/joker.json +2 -0
- package/themes/marvin.json +2 -0
- package/themes/mean.json +2 -0
- package/themes/military.json +2 -0
- package/themes/neutral.json +2 -0
- package/themes/pirate.json +2 -0
- package/themes/sports.json +2 -0
- package/themes/verstappen.json +2 -0
- package/themes/wizard.json +2 -0
- package/web/dist/assets/{Activity-B49fE_ug.js → Activity-BTCG2gMl.js} +1 -1
- package/web/dist/assets/{ActivityDetail-abt7hzD2.js → ActivityDetail-julsmPDB.js} +1 -1
- package/web/dist/assets/CatalogRow-DqS4e5XM.js +1 -0
- package/web/dist/assets/Diff-CIrGbiqm.js +5 -0
- package/web/dist/assets/Doctor-CpI53oHY.js +1 -0
- package/web/dist/assets/{Graph-DvYMtlvl.js → Graph-CS4tA7YS.js} +1 -1
- package/web/dist/assets/Harness-vRYs11kX.js +1 -0
- package/web/dist/assets/Laws-BjbHJrJt.js +3 -0
- package/web/dist/assets/Library-CjC7BAWN.js +1 -0
- package/web/dist/assets/{marked.esm-JKvyS0Pp.js → Markdown-kgpg9EQM.js} +15 -15
- package/web/dist/assets/Profile-CHr-PPf6.js +1 -0
- package/web/dist/assets/{Rules-CfsV1EVy.js → Rules-CsC6hfqk.js} +1 -1
- package/web/dist/assets/Skills-BvatJGfe.js +1 -0
- package/web/dist/assets/{StatusBadge-C9bZOEZk.js → StatusBadge-gRrak0nO.js} +1 -1
- package/web/dist/assets/{activity-Cc0n2jwx.js → activity-B0-FXZCj.js} +1 -1
- package/web/dist/assets/index-BnRWQbZb.js +12 -0
- package/web/dist/assets/index-Bx3I7cRW.js +2 -0
- package/web/dist/assets/index-D0f9tJlA.css +1 -0
- package/web/dist/assets/index-DJOhOwwR.js +1 -0
- package/web/dist/index.html +2 -2
- package/web/src/pages/Laws.jsx +42 -71
- package/web/dist/assets/Diff-D3_bVkih.js +0 -5
- package/web/dist/assets/Doctor-hXp7srsx.js +0 -1
- package/web/dist/assets/Harness-BIWei7N5.js +0 -1
- package/web/dist/assets/Laws-CxmnRdwt.js +0 -3
- package/web/dist/assets/Library-MjbOkHIW.js +0 -1
- package/web/dist/assets/Markdown-Dx_w0faR.js +0 -1
- package/web/dist/assets/Profile-BRFvv2gr.js +0 -1
- package/web/dist/assets/Skills-DArA6IHS.js +0 -1
- package/web/dist/assets/index-Bx3oxZ1o.css +0 -1
- package/web/dist/assets/index-DC3-GlCY.js +0 -1
- package/web/dist/assets/index-DP035wPa.js +0 -12
- 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 —
|
|
3
|
-
* `
|
|
4
|
-
*
|
|
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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
-
/**
|
|
42
|
+
/** A requested catalog section or item that does not exist. */
|
|
85
43
|
export class NotFound extends Error {}
|
|
86
44
|
|
|
87
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
* `
|
|
103
|
-
*
|
|
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
|
-
*
|
|
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
|
|
77
|
+
* drop both.
|
|
124
78
|
*/
|
|
125
79
|
export function webState(theme = null, target = null) {
|
|
126
80
|
const st = {
|
|
127
|
-
// `
|
|
128
|
-
//
|
|
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
|
-
*
|
|
168
|
-
*
|
|
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
|
-
*
|
|
171
|
-
*
|
|
172
|
-
* `
|
|
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 { /*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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`.
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
274
|
+
* Matched case-insensitively on Windows.
|
|
325
275
|
*
|
|
326
|
-
* `comparePaths`
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
400
|
-
*
|
|
401
|
-
*
|
|
402
|
-
*
|
|
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`
|
|
409
|
-
* caller
|
|
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
|
|
413
|
-
*
|
|
414
|
-
*
|
|
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
|
-
*
|
|
426
|
-
*
|
|
365
|
+
* `$GENESEED_WIKI` first, else `wiki.jsonc` beside the deployed bundle, read with the
|
|
366
|
+
* harness's generic JSONC loader.
|
|
427
367
|
*
|
|
428
|
-
* `resolvePath
|
|
429
|
-
*
|
|
430
|
-
*
|
|
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
|
-
*
|
|
443
|
-
*
|
|
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
|
|
446
|
-
*
|
|
447
|
-
*
|
|
448
|
-
*
|
|
449
|
-
*
|
|
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
|
-
/**
|
|
462
|
-
|
|
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
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
|
481
|
-
// every OTHER vault in it.
|
|
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
|
-
//
|
|
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
|
-
/**
|
|
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
|
-
//
|
|
526
|
-
//
|
|
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
|
-
*
|
|
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
|
-
*
|
|
547
|
-
*
|
|
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
|
-
*
|
|
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
|
-
/** `
|
|
501
|
+
/** `js/web/graph.mjs` is a second reader of this pattern. */
|
|
567
502
|
export const WIKILINK_RE = /\[\[([^\]]+)\]\]/g;
|
|
568
503
|
|
|
569
504
|
/**
|
|
570
|
-
* `
|
|
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
|
-
*
|
|
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()` —
|
|
586
|
-
//
|
|
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
|
-
*
|
|
598
|
-
*
|
|
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
|
-
*
|
|
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
|
-
/** `
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
718
|
+
/** The union across every global install, verbatim. */
|
|
786
719
|
export const apiExcludes = () => excludesSnapshot();
|
|
787
720
|
|
|
788
721
|
/**
|
|
789
|
-
*
|
|
790
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
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 { /*
|
|
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
|
|
948
|
-
* because NOTHING else in this API carries a per-entry date: every
|
|
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
|
-
*
|
|
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,
|
|
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
|
-
//
|
|
1023
|
-
//
|
|
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
|
|
1027
|
-
if (
|
|
1028
|
-
return apiItem(state,
|
|
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
|
-
//
|
|
1031
|
-
//
|
|
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
|
-
/**
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
1066
|
-
// indices, and the fingerprint every mutation must send back is what this GET
|
|
1067
|
-
//
|
|
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
|
};
|