geneseed 3.1.1 → 3.1.3

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 (106) hide show
  1. package/CHANGELOG.md +123 -1
  2. package/DESIGN.md +15 -15
  3. package/README.md +1 -1
  4. package/SETUP.md +1 -1
  5. package/SHIPPED.md +1 -1
  6. package/bin/{geneseed.mjs → build-driver.mjs} +39 -42
  7. package/bin/geneseed-cli.mjs +33 -32
  8. package/bin/geneseed-hook.mjs +7 -7
  9. package/docs/design-history.md +40 -40
  10. package/docs/extending.md +111 -83
  11. package/docs/web/exclusions.md +1 -1
  12. package/docs/web/footprint.md +1 -1
  13. package/docs/web/foreman-mode.md +2 -2
  14. package/docs/web/themes-author.md +1 -1
  15. package/docs/web-ui.md +41 -29
  16. package/harness.config.json +1 -1
  17. package/js/README.md +203 -0
  18. package/js/build/bundle.mjs +231 -0
  19. package/js/{catalog.mjs → build/catalog.mjs} +7 -7
  20. package/js/build/emit-claude.mjs +287 -0
  21. package/js/build/emit-common.mjs +255 -0
  22. package/js/build/emit-opencode.mjs +214 -0
  23. package/js/{generate.mjs → build/generate.mjs} +27 -28
  24. package/js/{render.mjs → build/render.mjs} +13 -9
  25. package/js/{checkout.mjs → build/source.mjs} +9 -9
  26. package/js/build/stubs.mjs +256 -0
  27. package/js/{themes.mjs → build/themes.mjs} +24 -24
  28. package/js/build/version.mjs +155 -0
  29. package/js/{hooks.mjs → hosts/hooks.mjs} +47 -44
  30. package/js/{hosts.mjs → hosts/hosts.mjs} +30 -28
  31. package/js/{installs.mjs → hosts/installs.mjs} +18 -16
  32. package/js/{link.mjs → hosts/link.mjs} +29 -28
  33. package/js/{mcp.mjs → hosts/mcp.mjs} +10 -9
  34. package/js/{native.mjs → hosts/native.mjs} +24 -22
  35. package/js/{opencode.mjs → hosts/opencode.mjs} +12 -7
  36. package/js/{settings.mjs → hosts/settings.mjs} +63 -55
  37. package/js/inspect/checks-authoring.mjs +761 -0
  38. package/js/inspect/checks-build.mjs +231 -0
  39. package/js/inspect/checks-repo.mjs +308 -0
  40. package/js/{diff.mjs → inspect/diff.mjs} +34 -31
  41. package/js/inspect/doctor.mjs +286 -0
  42. package/js/{excludes.mjs → inspect/excludes.mjs} +30 -33
  43. package/js/{inventory.mjs → inspect/inventory.mjs} +20 -17
  44. package/js/{registry.mjs → inspect/registry.mjs} +12 -10
  45. package/js/inspect/scan.mjs +125 -0
  46. package/js/{status.mjs → inspect/status.mjs} +27 -25
  47. package/js/inspect/validate.mjs +199 -0
  48. package/js/lib/fs.mjs +37 -759
  49. package/js/lib/json.mjs +344 -0
  50. package/js/lib/paths.mjs +242 -0
  51. package/js/lib/proc.mjs +3 -3
  52. package/js/lib/text.mjs +181 -0
  53. package/js/lib/udiff.mjs +1 -1
  54. package/js/{memory.mjs → maintain/memory.mjs} +10 -10
  55. package/js/{migrate.mjs → maintain/migrate.mjs} +28 -26
  56. package/js/{setup.mjs → maintain/setup.mjs} +34 -32
  57. package/js/{uninstall.mjs → maintain/uninstall.mjs} +55 -56
  58. package/js/{update.mjs → maintain/update.mjs} +51 -51
  59. package/js/{anim.mjs → ui/anim.mjs} +30 -28
  60. package/js/{cli.mjs → ui/cli.mjs} +10 -10
  61. package/js/{menu.mjs → ui/menu.mjs} +10 -9
  62. package/js/{tui.mjs → ui/tui.mjs} +36 -34
  63. package/js/web/actions.mjs +84 -85
  64. package/js/web/activity.mjs +22 -21
  65. package/js/web/api.mjs +154 -51
  66. package/js/web/daemon.mjs +319 -0
  67. package/js/web/docs.mjs +24 -21
  68. package/js/web/graph.mjs +0 -0
  69. package/js/web/handler.mjs +442 -0
  70. package/js/web/jobs.mjs +21 -19
  71. package/js/web/routes.mjs +245 -0
  72. package/js/web/server.mjs +56 -987
  73. package/package.json +11 -3
  74. package/src/doctrines/README.md +1 -1
  75. package/themes/opencode/README.md +1 -1
  76. package/web/dist/assets/{Activity-D_sYBlW-.js → Activity-B49fE_ug.js} +1 -1
  77. package/web/dist/assets/{ActivityDetail-BomKcMhy.js → ActivityDetail-abt7hzD2.js} +1 -1
  78. package/web/dist/assets/{Diff-BSvi1WUg.js → Diff-D3_bVkih.js} +1 -1
  79. package/web/dist/assets/{Doctor-Cz1s-y1n.js → Doctor-hXp7srsx.js} +1 -1
  80. package/web/dist/assets/{Graph-CuntzFJ0.js → Graph-DvYMtlvl.js} +1 -1
  81. package/web/dist/assets/Harness-BIWei7N5.js +1 -0
  82. package/web/dist/assets/Laws-CxmnRdwt.js +3 -0
  83. package/web/dist/assets/{Library-DXhTN9ju.js → Library-MjbOkHIW.js} +1 -1
  84. package/web/dist/assets/{Markdown-BoiZcdUl.js → Markdown-Dx_w0faR.js} +1 -1
  85. package/web/dist/assets/{Profile-DfCHlZ03.js → Profile-BRFvv2gr.js} +1 -1
  86. package/web/dist/assets/{Rules-eND7hJv9.js → Rules-CfsV1EVy.js} +1 -1
  87. package/web/dist/assets/{Skills-CDl9opZO.js → Skills-DArA6IHS.js} +1 -1
  88. package/web/dist/assets/{StatusBadge-DSDtcArX.js → StatusBadge-C9bZOEZk.js} +1 -1
  89. package/web/dist/assets/{activity-Dmb0FTAx.js → activity-Cc0n2jwx.js} +1 -1
  90. package/web/dist/assets/index-Bx3oxZ1o.css +1 -0
  91. package/web/dist/assets/index-DC3-GlCY.js +1 -0
  92. package/web/dist/assets/index-DP035wPa.js +12 -0
  93. package/web/dist/assets/{index-Dg58ScBe.js → index-dHi-7M-F.js} +1 -1
  94. package/web/dist/index.html +2 -2
  95. package/web/src/pages/Laws.jsx +5 -19
  96. package/docs/declined.md +0 -200
  97. package/docs/limits.md +0 -329
  98. package/js/doctor.mjs +0 -1750
  99. package/js/emit.mjs +0 -1411
  100. package/web/dist/assets/About-DyK7yRy7.js +0 -1
  101. package/web/dist/assets/Harnesses-DgRyOviS.js +0 -1
  102. package/web/dist/assets/Laws-CWk3Mpri.js +0 -3
  103. package/web/dist/assets/Themes-CoAWuIeM.js +0 -2
  104. package/web/dist/assets/index-B-mXquBZ.css +0 -1
  105. package/web/dist/assets/index-CH_Z725C.js +0 -12
  106. package/web/dist/assets/index-jrY4m9jP.js +0 -1
@@ -0,0 +1,245 @@
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
+ /**
189
+ * GET routes THAT NEVER EXISTED ON THE REFERENCE — the same fourth part of the partition, one
190
+ * verb over, and it exists for the reason `POST_BEYOND_REF` above spells out in full.
191
+ *
192
+ * `test_every_get_route_is_either_ported_or_declared_unported` asserts SET EQUALITY against
193
+ * `REF_GET`, a frozen literal of the paths the Python daemon answered. Widening that list to
194
+ * admit a route added in 2026 would forge the record; leaving the equality unqualified would
195
+ * mean this daemon may never grow a GET. So additions are enumerated here instead, and the
196
+ * gate compares the covered set against the reference's surface PLUS this one.
197
+ *
198
+ * `/api/recent` is the first, and it is a genuine gap rather than a convenience: no catalog row
199
+ * on the whole surface carries a date, and a browser cannot stat the `source` path it is handed
200
+ * — so "what changed lately" had no answer that did not involve the server looking. See
201
+ * `apiRecent` in `api.mjs` for what it will and will not date.
202
+ *
203
+ * A route belongs here only once it is dispatched; the dispatcher probe is what keeps this from
204
+ * becoming a declaration nobody honours.
205
+ */
206
+ export const GET_BEYOND_REF = new Set([
207
+ '/api/recent',
208
+ ]);
209
+
210
+ export function notPorted(path) {
211
+ return NOT_PORTED.has(path) || NOT_PORTED_PREFIXES.some((p) => path.startsWith(p));
212
+ }
213
+
214
+ export function notPortedPost(path) {
215
+ return NOT_PORTED_POST.has(path) || DECLINED_POST.has(path)
216
+ || NOT_PORTED_POST_PREFIXES.some((p) => path.startsWith(p));
217
+ }
218
+
219
+ /**
220
+ * `Handler._read_json_body` — the drained body as a dict, `{}` for ANYTHING else.
221
+ *
222
+ * P6a retained the drain with a note that it was measured indistinguishable and stayed
223
+ * because "`_read_json_body` needs the bytes from P6f on". This is that caller.
224
+ *
225
+ * `{}` AND NEVER A RAISE is the contract, and it is what makes a malformed body a 409 or a
226
+ * 404 from the endpoint's own validation rather than a 500 from a parser. That matters
227
+ * beyond tidiness: P5a's one un-portable line is a `json` error message, because CPython
228
+ * and V8 word them differently AND report different offsets — so a 500 carrying a parse
229
+ * error is a response the two implementations can never agree on.
230
+ *
231
+ * `parseJson`, not `JSON.parse`: a body value can be written straight into a user's file
232
+ * (`api_profile_save`'s `text`, `api_rules_mutate`'s `title`) and `str(1.0)` is `"1.0"`
233
+ * where `String(1)` is `"1"`. A non-object body — a bare list, a number, `null` — takes the
234
+ * same `{}` arm as garbage, because `isinstance(obj, dict)` is what the reference asks.
235
+ */
236
+ export function readJsonBody(buf) {
237
+ let obj;
238
+ try {
239
+ obj = parseJson((buf && buf.length ? buf : Buffer.from('{}')).toString('utf-8'));
240
+ } catch {
241
+ return {};
242
+ }
243
+ return (obj !== null && typeof obj === 'object' && obj.constructor === Object) ? obj : {};
244
+ }
245
+