geneseed 3.1.1 → 3.1.2
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 +68 -1
- package/DESIGN.md +15 -15
- package/README.md +1 -1
- package/SETUP.md +1 -1
- package/SHIPPED.md +1 -1
- package/bin/{geneseed.mjs → build-driver.mjs} +39 -42
- package/bin/geneseed-cli.mjs +33 -32
- package/bin/geneseed-hook.mjs +7 -7
- package/docs/design-history.md +40 -40
- package/docs/extending.md +111 -83
- package/harness.config.json +1 -1
- package/js/README.md +203 -0
- package/js/build/bundle.mjs +231 -0
- package/js/{catalog.mjs → build/catalog.mjs} +7 -7
- package/js/build/emit-claude.mjs +287 -0
- package/js/build/emit-common.mjs +255 -0
- package/js/build/emit-opencode.mjs +214 -0
- package/js/{generate.mjs → build/generate.mjs} +27 -28
- package/js/{render.mjs → build/render.mjs} +13 -9
- package/js/{checkout.mjs → build/source.mjs} +9 -9
- package/js/build/stubs.mjs +256 -0
- package/js/{themes.mjs → build/themes.mjs} +24 -24
- package/js/build/version.mjs +155 -0
- package/js/{hooks.mjs → hosts/hooks.mjs} +47 -44
- package/js/{hosts.mjs → hosts/hosts.mjs} +30 -28
- package/js/{installs.mjs → hosts/installs.mjs} +18 -16
- package/js/{link.mjs → hosts/link.mjs} +29 -28
- package/js/{mcp.mjs → hosts/mcp.mjs} +10 -9
- package/js/{native.mjs → hosts/native.mjs} +24 -22
- package/js/{opencode.mjs → hosts/opencode.mjs} +12 -7
- package/js/{settings.mjs → hosts/settings.mjs} +63 -55
- package/js/inspect/checks-authoring.mjs +761 -0
- package/js/inspect/checks-build.mjs +231 -0
- package/js/inspect/checks-repo.mjs +308 -0
- package/js/{diff.mjs → inspect/diff.mjs} +34 -31
- package/js/inspect/doctor.mjs +286 -0
- package/js/{excludes.mjs → inspect/excludes.mjs} +30 -33
- package/js/{inventory.mjs → inspect/inventory.mjs} +20 -17
- package/js/{registry.mjs → inspect/registry.mjs} +12 -10
- package/js/inspect/scan.mjs +125 -0
- package/js/{status.mjs → inspect/status.mjs} +27 -25
- package/js/inspect/validate.mjs +199 -0
- package/js/lib/fs.mjs +37 -759
- package/js/lib/json.mjs +344 -0
- package/js/lib/paths.mjs +242 -0
- package/js/lib/proc.mjs +3 -3
- package/js/lib/text.mjs +181 -0
- package/js/lib/udiff.mjs +1 -1
- package/js/{memory.mjs → maintain/memory.mjs} +10 -10
- package/js/{migrate.mjs → maintain/migrate.mjs} +28 -26
- package/js/{setup.mjs → maintain/setup.mjs} +34 -32
- package/js/{uninstall.mjs → maintain/uninstall.mjs} +55 -56
- package/js/{update.mjs → maintain/update.mjs} +51 -51
- package/js/{anim.mjs → ui/anim.mjs} +30 -28
- package/js/{cli.mjs → ui/cli.mjs} +10 -10
- package/js/{menu.mjs → ui/menu.mjs} +10 -9
- package/js/{tui.mjs → ui/tui.mjs} +36 -34
- package/js/web/actions.mjs +84 -85
- package/js/web/activity.mjs +22 -21
- package/js/web/api.mjs +72 -50
- package/js/web/daemon.mjs +319 -0
- package/js/web/docs.mjs +24 -21
- package/js/web/graph.mjs +0 -0
- package/js/web/handler.mjs +442 -0
- package/js/web/jobs.mjs +21 -19
- package/js/web/routes.mjs +223 -0
- package/js/web/server.mjs +56 -987
- package/package.json +11 -3
- package/src/doctrines/README.md +1 -1
- package/themes/opencode/README.md +1 -1
- package/web/dist/assets/{About-DyK7yRy7.js → About-CPvIFZGh.js} +1 -1
- package/web/dist/assets/{Activity-D_sYBlW-.js → Activity-z0uXdS7J.js} +1 -1
- package/web/dist/assets/{ActivityDetail-BomKcMhy.js → ActivityDetail-CEj5IJpF.js} +1 -1
- package/web/dist/assets/{Diff-BSvi1WUg.js → Diff-BMR8fPuG.js} +1 -1
- package/web/dist/assets/{Doctor-Cz1s-y1n.js → Doctor-Bz5qE4r9.js} +1 -1
- package/web/dist/assets/{Graph-CuntzFJ0.js → Graph-BV6a6t2l.js} +1 -1
- package/web/dist/assets/{Harnesses-DgRyOviS.js → Harnesses-C5Ag5PaQ.js} +1 -1
- package/web/dist/assets/{Laws-CWk3Mpri.js → Laws-BK07NKw9.js} +1 -1
- package/web/dist/assets/{Library-DXhTN9ju.js → Library-CiXN9sC3.js} +1 -1
- package/web/dist/assets/{Markdown-BoiZcdUl.js → Markdown-DG6FjUz-.js} +1 -1
- package/web/dist/assets/{Profile-DfCHlZ03.js → Profile-B2oSvdxH.js} +1 -1
- package/web/dist/assets/{Rules-eND7hJv9.js → Rules-DobVPd80.js} +1 -1
- package/web/dist/assets/{Skills-CDl9opZO.js → Skills-DQPsvsn9.js} +1 -1
- package/web/dist/assets/{StatusBadge-DSDtcArX.js → StatusBadge-BhJ3HhdI.js} +1 -1
- package/web/dist/assets/{Themes-CoAWuIeM.js → Themes-BP38FBU9.js} +1 -1
- package/web/dist/assets/{activity-Dmb0FTAx.js → activity-D9mfv7pY.js} +1 -1
- package/web/dist/assets/index-BE226Gv6.css +1 -0
- package/web/dist/assets/{index-jrY4m9jP.js → index-BZ7cPLMI.js} +1 -1
- package/web/dist/assets/index-BgIs5eIa.js +12 -0
- package/web/dist/assets/{index-Dg58ScBe.js → index-DGM5j2hB.js} +1 -1
- package/web/dist/index.html +2 -2
- package/web/src/pages/Laws.jsx +1 -1
- package/docs/declined.md +0 -200
- package/docs/limits.md +0 -329
- package/js/doctor.mjs +0 -1750
- package/js/emit.mjs +0 -1411
- package/web/dist/assets/index-B-mXquBZ.css +0 -1
- package/web/dist/assets/index-CH_Z725C.js +0 -12
package/js/web/jobs.mjs
CHANGED
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
* the chain on the first non-zero. In-process that would be a return value threaded through
|
|
34
34
|
* every verb's error paths, several of which currently end at `process.exitCode`.
|
|
35
35
|
*
|
|
36
|
-
* SO THE ARGV IS `node bin/geneseed-cli.mjs <verb>` — or `node bin/
|
|
36
|
+
* SO THE ARGV IS `node bin/geneseed-cli.mjs <verb>` — or `node bin/build-driver.mjs <build args>`
|
|
37
37
|
* for the three rows that run the GENERATOR, which is `build.py`'s twin and the same program
|
|
38
38
|
* by a different entry. `process.execPath` is the running interpreter by absolute path, so
|
|
39
39
|
* stripping `node` off PATH cannot change what starts. Nothing here can name `python`.
|
|
@@ -47,15 +47,16 @@
|
|
|
47
47
|
* primitive, not this program.
|
|
48
48
|
*
|
|
49
49
|
* ---------------------------------------------------------------------------------------
|
|
50
|
-
* THE PARTITION —
|
|
50
|
+
* THE PARTITION — all eight action rows cross, and the declaration outlives them.
|
|
51
51
|
*
|
|
52
|
-
* `doctor`, `build`, `build-all`, `export`, `uninstall` and
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
52
|
+
* `doctor`, `build`, `build-all`, `export`, `uninstall`, `update`, `link` and `unlink` each
|
|
53
|
+
* name a verb `bin/geneseed-cli.mjs` runs. `link`/`unlink` were the last two: they ran
|
|
54
|
+
* `harness.py link|unlink` until P10b, and a runner that spawned the Python harness would
|
|
55
|
+
* have put Python back into a "no Python needed" install.
|
|
56
|
+
*
|
|
57
|
+
* `NOT_PORTED_ACTIONS` IS EMPTY AND STILL DECLARED, which is the point of it: a ninth action
|
|
58
|
+
* added later cannot quietly answer "unknown action", because `tests/unit/web_jobs.test.mjs`
|
|
59
|
+
* holds the two sets against `actionCommands`' own keys.
|
|
59
60
|
*
|
|
60
61
|
* ---------------------------------------------------------------------------------------
|
|
61
62
|
* THREE DETAILS OF `_run`, EACH A COMMENT EXPLAINING A BUG IT ALREADY FIXED.
|
|
@@ -64,7 +65,7 @@
|
|
|
64
65
|
* the daemon mid-job — which killed the job's own tracking and left the console on `running`
|
|
65
66
|
* forever. It was set here before any ported row read it, because it was P6g's CONTRACT WITH
|
|
66
67
|
* P8, and a contract written after the fact is a contract that was broken once first. P8a is
|
|
67
|
-
* where `js/update.mjs` began honouring it, and P8c is where the `update` row that triggers
|
|
68
|
+
* where `js/maintain/update.mjs` began honouring it, and P8c is where the `update` row that triggers
|
|
68
69
|
* it crossed — so it is a live pair now rather than a promise.
|
|
69
70
|
*
|
|
70
71
|
* `PYTHONUNBUFFERED=1` reaches the Python child AND its own Python children, or their stdout
|
|
@@ -107,10 +108,11 @@ import { statSync } from 'node:fs';
|
|
|
107
108
|
import { StringDecoder } from 'node:string_decoder';
|
|
108
109
|
import path from 'node:path';
|
|
109
110
|
|
|
110
|
-
import { ROOT, PACK_ORDER } from '../
|
|
111
|
-
import { setupBuildArgs } from '../generate.mjs';
|
|
112
|
-
import { isDict } from '../mcp.mjs';
|
|
113
|
-
import {
|
|
111
|
+
import { ROOT, PACK_ORDER } from '../build/source.mjs';
|
|
112
|
+
import { setupBuildArgs } from '../build/generate.mjs';
|
|
113
|
+
import { isDict } from '../hosts/mcp.mjs';
|
|
114
|
+
import { readText, writeText } from '../lib/fs.mjs';
|
|
115
|
+
import { jsonDumpsCompact, parseJson, isTruthy } from '../lib/json.mjs';
|
|
114
116
|
|
|
115
117
|
const isFile = (p) => { try { return statSync(p).isFile(); } catch { return false; } };
|
|
116
118
|
|
|
@@ -232,7 +234,7 @@ export class JobManager {
|
|
|
232
234
|
return;
|
|
233
235
|
}
|
|
234
236
|
for (const j of Array.isArray(jobs) ? jobs : []) {
|
|
235
|
-
if (isDict(j) &&
|
|
237
|
+
if (isDict(j) && isTruthy(j.id) && j.status !== 'running') this._jobs.set(String(j.id), j);
|
|
236
238
|
}
|
|
237
239
|
}
|
|
238
240
|
|
|
@@ -401,7 +403,7 @@ export class JobManager {
|
|
|
401
403
|
* five. `update` and `export` self-resolve the deployed theme downstream and take no args.
|
|
402
404
|
*
|
|
403
405
|
* THE ARGV HEAD IS THE PORT. The reference names `sys.executable` and `rituals/harness.py` (or
|
|
404
|
-
* `build.py`); this names `process.execPath` and `bin/geneseed-cli.mjs` (or `bin/
|
|
406
|
+
* `build.py`); this names `process.execPath` and `bin/geneseed-cli.mjs` (or `bin/build-driver.mjs`,
|
|
405
407
|
* which is `build.py`'s twin). The two can never be byte-equal, `tests/web_golden.py`
|
|
406
408
|
* normalises the `$ <argv>` echo line for that reason, and `tests/test_web_jobs.py` asserts
|
|
407
409
|
* each side's head ABSOLUTELY while comparing the tails literally — which is where a real port
|
|
@@ -409,7 +411,7 @@ export class JobManager {
|
|
|
409
411
|
*/
|
|
410
412
|
const NODE = () => process.execPath;
|
|
411
413
|
const CLI = () => path.join(ROOT, 'bin', 'geneseed-cli.mjs');
|
|
412
|
-
const GEN = () => path.join(ROOT, 'bin', '
|
|
414
|
+
const GEN = () => path.join(ROOT, 'bin', 'build-driver.mjs');
|
|
413
415
|
|
|
414
416
|
/**
|
|
415
417
|
* The rows whose VERB has not crossed. **EMPTY SINCE P10b, AND STILL DECLARED.**
|
|
@@ -417,7 +419,7 @@ const GEN = () => path.join(ROOT, 'bin', 'geneseed.mjs');
|
|
|
417
419
|
* P6g opened it with three rows and wrote the rule that closes it: each removed by the phase
|
|
418
420
|
* that ports its verb. `update` left in P8c; `link` and `unlink` left in P10b, which is the
|
|
419
421
|
* phase that also had to decide whether those two verbs should exist under npm at all
|
|
420
|
-
* (`js/link.mjs`'s docblock carries the argument, and the deciding vote was these two rows —
|
|
422
|
+
* (`js/hosts/link.mjs`'s docblock carries the argument, and the deciding vote was these two rows —
|
|
421
423
|
* the console's Settings buttons POST here, and only a ported verb can answer them).
|
|
422
424
|
*
|
|
423
425
|
* WHY AN EMPTY SET RATHER THAN A DELETED ONE. The `ast` cross-check in
|
|
@@ -473,7 +475,7 @@ function actionTable({
|
|
|
473
475
|
// P8c. The row names `upgrade`, which is the VERB; `update` is the action name here and
|
|
474
476
|
// argparse's alias there, and the reference's own row is `[py, harness.py, "upgrade"]` for
|
|
475
477
|
// the same reason. The daemon bounce this job must not do itself is `GENESEED_WEB_JOB`'s
|
|
476
|
-
// job, set by `_run` above and read by `js/update.mjs` — P6g's contract with P8, honoured.
|
|
478
|
+
// job, set by `_run` above and read by `js/maintain/update.mjs` — P6g's contract with P8, honoured.
|
|
477
479
|
update: [[NODE(), CLI(), 'upgrade']],
|
|
478
480
|
export: [[NODE(), CLI(), 'diff', '--out']],
|
|
479
481
|
// Local-machine maintenance, surfaced in the web Settings. uninstall keeps memory (never
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHAT THE WEB API ANSWERS — as a declaration, separate from the dispatcher that honours it.
|
|
3
|
+
*
|
|
4
|
+
* Eight exported sets and one map. They are not documentation: `tests/unit/web_server.mjs` reads
|
|
5
|
+
* them as a TWO-SIDED PARTITION and probes the real handler against them, because a declaration is
|
|
6
|
+
* not a dispatcher — mutation M23 exists precisely because a route can be declared in one and
|
|
7
|
+
* missing from the other, and nothing but a probe notices.
|
|
8
|
+
*
|
|
9
|
+
* Split out of `server.mjs` because it is the half a reader consults to answer "does the console
|
|
10
|
+
* expose X?", which has nothing to do with sockets, daemons or gzip.
|
|
11
|
+
*/
|
|
12
|
+
import { formatValue, isTruthy, parseJson } from '../lib/json.mjs';
|
|
13
|
+
import { apiExcludesMutate, apiInstallToggle, apiMcpToggle, apiMemoryDelete, apiProfileSave, apiRulesMutate, apiRulesPromote, apiSelectView } from './actions.mjs';
|
|
14
|
+
import { apiActivityToggle } from './activity.mjs';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The API paths this phase does not answer yet.
|
|
18
|
+
*
|
|
19
|
+
* Without it every one of them would fall through to `serveStatic`, which answers ANY
|
|
20
|
+
* unknown path with index.html and a 200 — the SPA fallback. An unported endpoint would
|
|
21
|
+
* therefore return a plausible HTML page instead of failing, and the first cell written
|
|
22
|
+
* against it in P6b would be the thing that discovered the route was missing. A 501
|
|
23
|
+
* naming the phase costs one lookup and cannot be mistaken for a working endpoint.
|
|
24
|
+
*
|
|
25
|
+
* It shrank to empty in P6g and the DECLARATION STAYS. Every GET the reference answers is now
|
|
26
|
+
* answered here, so the two sets hold nothing — but they are half of a partition
|
|
27
|
+
* (`test_every_get_route_is_either_ported_or_declared_unported`), and deleting them would let
|
|
28
|
+
* the next GET route added to the reference fall through to the SPA with nothing to say so.
|
|
29
|
+
* An empty declaration is the partition asserting that there is nothing left to declare.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* SPLIT BY VERB SINCE P6b, and the split is the point rather than a tidy-up. Five paths
|
|
33
|
+
* answer BOTH verbs with different bodies — `/api/mcp`, `/api/excludes`, `/api/rules`,
|
|
34
|
+
* `/api/profile`, `/api/activity` — and P6b ports the GET half of two of them. One set
|
|
35
|
+
* keyed on path alone would have taken `/api/excludes` out of the unported list the
|
|
36
|
+
* moment its GET landed, and its POST would then have answered `{"error": "not found"}`
|
|
37
|
+
* with a 404: a plausible-looking response where the reference returns 200 or 409, which
|
|
38
|
+
* is exactly the failure the 501 exists to prevent.
|
|
39
|
+
*/
|
|
40
|
+
/** EMPTY SINCE P6g — every GET the reference answers is answered here. */
|
|
41
|
+
export const NOT_PORTED = new Set([]);
|
|
42
|
+
export const NOT_PORTED_PREFIXES = [];
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The POSTs that have not crossed YET — and EMPTY SINCE P6i, which took `/api/install`.
|
|
46
|
+
*
|
|
47
|
+
* IT STAYS DECLARED, for the same reason the GET pair above stays and the prefix lists have
|
|
48
|
+
* stayed since P6g: this set is half of a partition
|
|
49
|
+
* (`test_every_post_route_is_either_ported_or_declared_unported`), so an empty set is the
|
|
50
|
+
* partition ASSERTING that there is nothing left to declare. Delete it and the next POST route
|
|
51
|
+
* added to the reference falls through to `{"error": "not found"}` at 404 — a plausible-looking
|
|
52
|
+
* answer where the reference returns 200 or 409 — with nothing to say so.
|
|
53
|
+
*
|
|
54
|
+
* THE DISPATCHER PROBE NEEDS A TARGET, which an empty set no longer supplies.
|
|
55
|
+
* `tests/test_web_server.py` retired its `unportedGet` probe when `NOT_PORTED` emptied in P6g;
|
|
56
|
+
* `unportedPost` is retired here for the same reason, and `declinedPost` (`/api/pick-folder`,
|
|
57
|
+
* which never crosses) is what keeps the 501 arm of the dispatcher under a live probe.
|
|
58
|
+
*/
|
|
59
|
+
export const NOT_PORTED_POST = new Set([]);
|
|
60
|
+
export const NOT_PORTED_POST_PREFIXES = [];
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* The POSTs that will NEVER cross, which is a different claim from the set above.
|
|
64
|
+
*
|
|
65
|
+
* `/api/pick-folder` opens an OS-NATIVE folder chooser on the daemon host — `osascript` on
|
|
66
|
+
* macOS, a one-shot `tkinter` subprocess elsewhere. There is no Node twin that is not a new
|
|
67
|
+
* GUI dependency, and reaching for one would also be a sixth `_ALLOWED_SPAWNS` row for a
|
|
68
|
+
* modal dialog. The UI already falls back to its editable path field when this endpoint
|
|
69
|
+
* fails, which is what a 501 gives it.
|
|
70
|
+
*
|
|
71
|
+
* SEPARATE FROM `NOT_PORTED_POST` ON PURPOSE. That set is a to-do list every later phase
|
|
72
|
+
* shrinks, and folding a permanent decline into it would make the list wrong in the one
|
|
73
|
+
* direction nobody checks: a phase that emptied it would have to either port a GUI dialog
|
|
74
|
+
* or quietly delete a declaration. Both sets are unioned into the partition the reference's
|
|
75
|
+
* routes are cross-checked against, so neither can drift.
|
|
76
|
+
*/
|
|
77
|
+
export const DECLINED_POST = new Set([
|
|
78
|
+
'/api/pick-folder',
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The POSTs P6f answers — and this table IS the dispatch, not a declaration beside it.
|
|
83
|
+
*
|
|
84
|
+
* M23's lesson, applied one verb over: `test_the_declared_partition_is_the_one_the_dispatcher_uses`
|
|
85
|
+
* exists because a hand-kept list can agree with the reference while the running handler
|
|
86
|
+
* does something else. `doPost` looks routes up HERE, and `PORTED_POST` is this table's own
|
|
87
|
+
* keys, so the two cannot drift — the same reason `STATE_ROUTES` is a table on the GET side.
|
|
88
|
+
*
|
|
89
|
+
* THE SECOND COLUMN IS THE 409 CONVENTION, and it is a column because it is not uniform.
|
|
90
|
+
* `ok: false` means the write did not happen — a stale fingerprint, a body that never
|
|
91
|
+
* parsed, nothing to remove — and the client must re-fetch rather than retry, which is what
|
|
92
|
+
* 409 says and 200 does not. Five routes map it. `/api/activity` does NOT: its flag write
|
|
93
|
+
* answers 200 whatever `ok` says, and a port that unified the rule would be wrong exactly
|
|
94
|
+
* there. `/api/memory/delete` carries no `ok` at all — it deletes, or it raises `NotFound`
|
|
95
|
+
* and the outer catch answers 404 — so it is `false` here for a third reason again.
|
|
96
|
+
*
|
|
97
|
+
* `NotFound` IS NOT CAUGHT PER ROUTE. The reference wraps six of these calls in their own
|
|
98
|
+
* `except NotFound` AND wraps the whole of `_post_routes` in another inside `do_POST`. The
|
|
99
|
+
* inner six are redundant — every one produces the same `{"error": "not found: …"}` at 404
|
|
100
|
+
* — so the twin keeps the outer one only, in `handler`, where `do_POST`'s is.
|
|
101
|
+
*/
|
|
102
|
+
export const POST_ROUTES = new Map([
|
|
103
|
+
['/api/mcp', [apiMcpToggle, true]],
|
|
104
|
+
// P6i. `_post_routes` wraps this one in its own `except NotFound` and then maps
|
|
105
|
+
// `ok: false` to 409 — the same shape as `/api/mcp` beside it, so it fits the table's
|
|
106
|
+
// second column exactly and needs no third inline dispatch. The inner catch is redundant
|
|
107
|
+
// (see this table's docblock); `handler`'s outer one answers identically.
|
|
108
|
+
['/api/install', [apiInstallToggle, true]],
|
|
109
|
+
['/api/excludes', [apiExcludesMutate, true]],
|
|
110
|
+
['/api/view', [apiSelectView, true]],
|
|
111
|
+
['/api/activity', [apiActivityToggle, false]],
|
|
112
|
+
// `(self._read_json_body().get("name") or "")` — the SHELL resolves the name, and the
|
|
113
|
+
// endpoint takes a bare slug rather than a body.
|
|
114
|
+
['/api/memory/delete',
|
|
115
|
+
[(state, b) => apiMemoryDelete(state, isTruthy(b.name) ? formatValue(b.name) : ''), false]],
|
|
116
|
+
['/api/rules', [apiRulesMutate, true]],
|
|
117
|
+
['/api/rules/promote', [apiRulesPromote, true]],
|
|
118
|
+
['/api/profile', [apiProfileSave, true]],
|
|
119
|
+
]);
|
|
120
|
+
|
|
121
|
+
export const PORTED_POST = [...POST_ROUTES.keys()];
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The 409 column, exported so it can be cross-checked against the reference's own `200 if
|
|
125
|
+
* res.get("ok") else 409` conditionals by `ast`.
|
|
126
|
+
*
|
|
127
|
+
* A MUTATION IS WHY THIS EXISTS. Giving `/api/activity` the 409 treatment survived the whole
|
|
128
|
+
* cell matrix: every toggle a cell can perform succeeds, and reaching the `ok: false` arm
|
|
129
|
+
* needs the flag write to raise — which the two runtimes word differently, so no byte
|
|
130
|
+
* comparison could hold it. The column is the thing under test and no cell can see it, so
|
|
131
|
+
* the gate reads it out of both implementations instead.
|
|
132
|
+
*/
|
|
133
|
+
export const POST_ROUTES_CONVENTION = Object.fromEntries(
|
|
134
|
+
[...POST_ROUTES].map(([p, [, okIs409]]) => [p, okIs409]),
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The GET paths the dispatcher answers OUTSIDE the two tables, declared so the partition
|
|
139
|
+
* cross-check can see them: `/api/ping` is the shell's own, and the two Docs routes take
|
|
140
|
+
* the `?harness=` query param, which a `(state)` table entry has no way to receive.
|
|
141
|
+
*
|
|
142
|
+
* A declaration is not a dispatcher — M23 — so
|
|
143
|
+
* `test_the_declared_partition_is_the_one_the_dispatcher_uses` probes all three against
|
|
144
|
+
* the running handler as well.
|
|
145
|
+
*/
|
|
146
|
+
export const PORTED_INLINE = ['/api/ping', '/api/docs', '/api/docs/page/',
|
|
147
|
+
'/api/jobs', '/api/jobs/'];
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The POSTs P6g answers outside `POST_ROUTES`, and they are outside it for a reason the
|
|
151
|
+
* table's SECOND COLUMN makes plain.
|
|
152
|
+
*
|
|
153
|
+
* `POST_ROUTES` maps a route to `(fn, okIs409)` — one handler taking `(state, body)`, one
|
|
154
|
+
* boolean deciding 200 against 409. The jobs routes fit neither half. `/api/jobs/<id>/cancel`
|
|
155
|
+
* answers 200 or 404 on a lookup, not on an `ok` field; `/api/actions/<x>` answers 202 with a
|
|
156
|
+
* job id, 409 when the runner is busy, 404 for an action the table does not name, 501 for one
|
|
157
|
+
* that has not crossed, 400 or 409 for a resolver's refusal, and 200 for `restore` — six
|
|
158
|
+
* statuses over one prefix, none of them read off `ok`. Bending the column to fit would make
|
|
159
|
+
* it lie about the five routes it currently describes exactly, so these dispatch beside the
|
|
160
|
+
* table and `tests/test_web_jobs.py` reads them out of `PORTED_POST_INLINE` instead.
|
|
161
|
+
*/
|
|
162
|
+
export const PORTED_POST_INLINE = ['/api/shutdown', '/api/restart', '/api/jobs/',
|
|
163
|
+
'/api/actions/'];
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* POST routes THAT NEVER EXISTED ON THE REFERENCE — the first of them, and the reason this set
|
|
167
|
+
* has to exist at all.
|
|
168
|
+
*
|
|
169
|
+
* `test_every_post_route_is_either_ported_or_declared_unported` asserts SET EQUALITY against
|
|
170
|
+
* `REF_POST`, a frozen literal of the paths the Python daemon answered. That was exactly right
|
|
171
|
+
* while the port was in flight: a route on one side and not the other was a defect either way.
|
|
172
|
+
* The port is finished and the reference is deleted, so the assertion now says something else —
|
|
173
|
+
* "this daemon may never grow a route" — which is a rule nobody chose.
|
|
174
|
+
*
|
|
175
|
+
* SO THE PARTITION GAINS A FOURTH PART RATHER THAN LOSING ITS EQUALITY. Widening `REF_POST` would
|
|
176
|
+
* have been the cheap edit and it would have been a lie: that list is a RECORD of what the
|
|
177
|
+
* reference answered, and a route added in 2026 was never in it. Keeping the record honest and
|
|
178
|
+
* declaring the additions beside it keeps both claims checkable — the reference's surface is
|
|
179
|
+
* still pinned, and everything past it is enumerated here instead of merely appearing.
|
|
180
|
+
*
|
|
181
|
+
* A route belongs here only once it is dispatched below; the dispatcher probe is what keeps this
|
|
182
|
+
* from becoming a declaration nobody honours.
|
|
183
|
+
*/
|
|
184
|
+
export const POST_BEYOND_REF = new Set([
|
|
185
|
+
'/api/reveal',
|
|
186
|
+
]);
|
|
187
|
+
|
|
188
|
+
export function notPorted(path) {
|
|
189
|
+
return NOT_PORTED.has(path) || NOT_PORTED_PREFIXES.some((p) => path.startsWith(p));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function notPortedPost(path) {
|
|
193
|
+
return NOT_PORTED_POST.has(path) || DECLINED_POST.has(path)
|
|
194
|
+
|| NOT_PORTED_POST_PREFIXES.some((p) => path.startsWith(p));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* `Handler._read_json_body` — the drained body as a dict, `{}` for ANYTHING else.
|
|
199
|
+
*
|
|
200
|
+
* P6a retained the drain with a note that it was measured indistinguishable and stayed
|
|
201
|
+
* because "`_read_json_body` needs the bytes from P6f on". This is that caller.
|
|
202
|
+
*
|
|
203
|
+
* `{}` AND NEVER A RAISE is the contract, and it is what makes a malformed body a 409 or a
|
|
204
|
+
* 404 from the endpoint's own validation rather than a 500 from a parser. That matters
|
|
205
|
+
* beyond tidiness: P5a's one un-portable line is a `json` error message, because CPython
|
|
206
|
+
* and V8 word them differently AND report different offsets — so a 500 carrying a parse
|
|
207
|
+
* error is a response the two implementations can never agree on.
|
|
208
|
+
*
|
|
209
|
+
* `parseJson`, not `JSON.parse`: a body value can be written straight into a user's file
|
|
210
|
+
* (`api_profile_save`'s `text`, `api_rules_mutate`'s `title`) and `str(1.0)` is `"1.0"`
|
|
211
|
+
* where `String(1)` is `"1"`. A non-object body — a bare list, a number, `null` — takes the
|
|
212
|
+
* same `{}` arm as garbage, because `isinstance(obj, dict)` is what the reference asks.
|
|
213
|
+
*/
|
|
214
|
+
export function readJsonBody(buf) {
|
|
215
|
+
let obj;
|
|
216
|
+
try {
|
|
217
|
+
obj = parseJson((buf && buf.length ? buf : Buffer.from('{}')).toString('utf-8'));
|
|
218
|
+
} catch {
|
|
219
|
+
return {};
|
|
220
|
+
}
|
|
221
|
+
return (obj !== null && typeof obj === 'object' && obj.constructor === Object) ? obj : {};
|
|
222
|
+
}
|
|
223
|
+
|