geneseed 3.1.0 → 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 +110 -0
- package/DESIGN.md +31 -20
- package/README.md +2 -2
- package/SETUP.md +30 -3
- package/SHIPPED.md +1 -1
- package/bin/{geneseed.mjs → build-driver.mjs} +88 -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 +113 -85
- package/docs/web/rules.md +2 -0
- package/docs/web/setup-choices.md +13 -1
- 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} +45 -31
- package/js/{render.mjs → build/render.mjs} +72 -11
- package/js/{checkout.mjs → build/source.mjs} +52 -11
- 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} +68 -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} +94 -69
- 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} +34 -20
- 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} +49 -37
- package/js/{uninstall.mjs → maintain/uninstall.mjs} +62 -58
- 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 +124 -89
- package/js/web/activity.mjs +22 -21
- package/js/web/api.mjs +88 -58
- 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 +29 -21
- package/js/web/routes.mjs +223 -0
- package/js/web/server.mjs +56 -987
- package/package.json +11 -3
- package/src/AGENT.md.tmpl +1 -1
- package/src/doctrines/README.md +1 -1
- package/themes/opencode/README.md +1 -1
- package/web/dist/assets/{About-_MnJcwb_.js → About-CPvIFZGh.js} +1 -1
- package/web/dist/assets/{Activity-DpjqO-FR.js → Activity-z0uXdS7J.js} +1 -1
- package/web/dist/assets/{ActivityDetail-Bvrjv8oW.js → ActivityDetail-CEj5IJpF.js} +1 -1
- package/web/dist/assets/{Diff-DZ0IehBV.js → Diff-BMR8fPuG.js} +1 -1
- package/web/dist/assets/{Doctor-cu4krooO.js → Doctor-Bz5qE4r9.js} +1 -1
- package/web/dist/assets/{Graph-DUOPY65j.js → Graph-BV6a6t2l.js} +1 -1
- package/web/dist/assets/{Harnesses-B5Osg-BF.js → Harnesses-C5Ag5PaQ.js} +1 -1
- package/web/dist/assets/Laws-BK07NKw9.js +3 -0
- package/web/dist/assets/{Library-8y69I3_z.js → Library-CiXN9sC3.js} +1 -1
- package/web/dist/assets/{Markdown-Ds5GS1OD.js → Markdown-DG6FjUz-.js} +1 -1
- package/web/dist/assets/{Profile-7Jxa7ZUX.js → Profile-B2oSvdxH.js} +1 -1
- package/web/dist/assets/{Rules-UZ0jWkjg.js → Rules-DobVPd80.js} +1 -1
- package/web/dist/assets/{Skills-Bgkl0xLd.js → Skills-DQPsvsn9.js} +1 -1
- package/web/dist/assets/{StatusBadge-Cx4caF_2.js → StatusBadge-BhJ3HhdI.js} +1 -1
- package/web/dist/assets/{Themes-De8az7Sv.js → Themes-BP38FBU9.js} +1 -1
- package/web/dist/assets/{activity-VDAdVQ3q.js → activity-D9mfv7pY.js} +1 -1
- package/web/dist/assets/index-BE226Gv6.css +1 -0
- package/web/dist/assets/{index-BKrjBRV4.js → index-BZ7cPLMI.js} +1 -1
- package/web/dist/assets/index-BgIs5eIa.js +12 -0
- package/web/dist/assets/{index-wNiNzFFQ.js → index-DGM5j2hB.js} +1 -1
- package/web/dist/index.html +2 -2
- package/web/src/pages/Laws.jsx +135 -69
- package/docs/declined.md +0 -200
- package/docs/limits.md +0 -329
- package/js/doctor.mjs +0 -1712
- package/js/emit.mjs +0 -1410
- package/web/dist/assets/Laws-D8DJgTBp.js +0 -3
- package/web/dist/assets/index-1pPSz7Kl.css +0 -1
- package/web/dist/assets/index-C-gb-q-r.js +0 -12
package/web/src/pages/Laws.jsx
CHANGED
|
@@ -15,7 +15,7 @@ import ErrorState from '../components/ErrorState.jsx'
|
|
|
15
15
|
//
|
|
16
16
|
// ⚠ SIX, THOUGH ONLY FOUR HAVE A MEMBER. `context` and `comms` lost theirs when the corpus became
|
|
17
17
|
// nine invariants — that material moved to the ontology and the doctrine packs, neither of which
|
|
18
|
-
// is classed here. The list stays six because it is the VOCABULARY that js/inventory.mjs's
|
|
18
|
+
// is classed here. The list stays six because it is the VOCABULARY that js/inspect/inventory.mjs's
|
|
19
19
|
// LAW_CLASSES publishes and doctor quotes verbatim; the PAGE renders only non-empty facets, which
|
|
20
20
|
// is a display decision and not a change to the taxonomy.
|
|
21
21
|
const LAW_CATS = {
|
|
@@ -126,13 +126,56 @@ function ruleName(rawTitle, romanNum, arabicNum) {
|
|
|
126
126
|
// One expandable row, shared by the invariant and doctrine bands: lazy-loads its full body via
|
|
127
127
|
// /api/item/law/<address> the first time it opens, cached on subsequent toggles. The address is
|
|
128
128
|
// the catalog's `name` — a Roman numeral or `<pack>.<n>` — never the display number.
|
|
129
|
-
function LawRow({ law, isOpen, onToggle }) {
|
|
129
|
+
function LawRow({ law, isOpen, onToggle, toggleCol = null }) {
|
|
130
130
|
const { data: detail } = useAsync(
|
|
131
131
|
() => (isOpen ? api.item('law', law.addr) : Promise.resolve(null)),
|
|
132
132
|
[isOpen, law.addr],
|
|
133
133
|
)
|
|
134
134
|
const body = detail?.body || law.ess
|
|
135
|
-
|
|
135
|
+
const expandRow = (
|
|
136
|
+
<>
|
|
137
|
+
{isOpen && (
|
|
138
|
+
<div className="law-expand">
|
|
139
|
+
{detail ? (
|
|
140
|
+
<p>
|
|
141
|
+
<LawText text={body} />
|
|
142
|
+
</p>
|
|
143
|
+
) : (
|
|
144
|
+
<p className="dim">Loading…</p>
|
|
145
|
+
)}
|
|
146
|
+
<div className="law-srcline">
|
|
147
|
+
$ geneseed law {law.addr} · {law.src}
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
)}
|
|
151
|
+
</>
|
|
152
|
+
)
|
|
153
|
+
// Doctrine rows expose two separate actions: the first four columns are one disclosure
|
|
154
|
+
// button, while the fifth is the selection switch. Keeping them as siblings means a switch
|
|
155
|
+
// click changes only the staged selection, never the open rule.
|
|
156
|
+
return toggleCol ? (
|
|
157
|
+
<>
|
|
158
|
+
<div
|
|
159
|
+
className={`law-row has-toggle ${isOpen ? 'on' : ''} ${law.off ? 'law-off' : ''}`}
|
|
160
|
+
style={{ '--cc': law.c }}
|
|
161
|
+
>
|
|
162
|
+
<button className="law-disclosure" onClick={onToggle} aria-expanded={isOpen}>
|
|
163
|
+
<span className="law-no">
|
|
164
|
+
<span className="x">›</span>
|
|
165
|
+
{law.pad}
|
|
166
|
+
</span>
|
|
167
|
+
<span className="law-name">{law.name}</span>
|
|
168
|
+
<span className="law-princ">{law.ess}</span>
|
|
169
|
+
<span className="law-class">
|
|
170
|
+
<span className="cdot" />
|
|
171
|
+
{law.catLabel}
|
|
172
|
+
</span>
|
|
173
|
+
</button>
|
|
174
|
+
<span className="toggle-col">{toggleCol}</span>
|
|
175
|
+
</div>
|
|
176
|
+
{expandRow}
|
|
177
|
+
</>
|
|
178
|
+
) : (
|
|
136
179
|
<>
|
|
137
180
|
<button
|
|
138
181
|
className={`law-row ${isOpen ? 'on' : ''} ${law.off ? 'law-off' : ''}`}
|
|
@@ -151,20 +194,7 @@ function LawRow({ law, isOpen, onToggle }) {
|
|
|
151
194
|
{law.catLabel}
|
|
152
195
|
</span>
|
|
153
196
|
</button>
|
|
154
|
-
{
|
|
155
|
-
<div className="law-expand">
|
|
156
|
-
{detail ? (
|
|
157
|
-
<p>
|
|
158
|
-
<LawText text={body} />
|
|
159
|
-
</p>
|
|
160
|
-
) : (
|
|
161
|
-
<p className="dim">Loading…</p>
|
|
162
|
-
)}
|
|
163
|
-
<div className="law-srcline">
|
|
164
|
-
$ geneseed law {law.addr} · {law.src}
|
|
165
|
-
</div>
|
|
166
|
-
</div>
|
|
167
|
-
)}
|
|
197
|
+
{expandRow}
|
|
168
198
|
</>
|
|
169
199
|
)
|
|
170
200
|
}
|
|
@@ -222,13 +252,14 @@ export default function Laws({ selected, overview, onAction }) {
|
|
|
222
252
|
// throws, taking the whole page down rather than just the control. Hence the defensive
|
|
223
253
|
// `data?.items` here and the plain (non-hook) helpers further down.
|
|
224
254
|
const allItems = data?.items || []
|
|
225
|
-
//
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
255
|
+
// ⚠ THE UNIT IS THE RULE, not the pack. The selection below is a list of rule ADDRESSES
|
|
256
|
+
// (`process.7`), and the pack axis is DERIVED from it when Apply builds the request — a
|
|
257
|
+
// pack with every rule off drops out of `--doctrines` entirely, so the rendered AGENT.md
|
|
258
|
+
// never carries a pack header with nothing under it.
|
|
259
|
+
const deployedRules = allItems
|
|
260
|
+
.filter((i) => i.tier === 'doctrine' && i.active !== false)
|
|
261
|
+
.map((i) => i.name)
|
|
262
|
+
const deployedKey = deployedRules.join(',')
|
|
232
263
|
const [picked, setPicked] = useState([])
|
|
233
264
|
// Re-sync on the VALUE, never on the payload's identity: `data` is a fresh object on every
|
|
234
265
|
// refetch, so an identity dependency would discard a half-made selection.
|
|
@@ -282,7 +313,10 @@ export default function Laws({ selected, overview, onAction }) {
|
|
|
282
313
|
pack: it.pack,
|
|
283
314
|
title: it.packTitle || it.pack,
|
|
284
315
|
desc: it.packDesc || '',
|
|
285
|
-
|
|
316
|
+
// ⚠ `packActive`, NOT the first rule's `active`. A pack is on when the install built
|
|
317
|
+
// it in AT ALL; with the per-rule axis its first rule can be the excluded one, and
|
|
318
|
+
// reading the pack's state off it would report a live pack as dropped.
|
|
319
|
+
active: it.packActive !== false,
|
|
286
320
|
rules: [],
|
|
287
321
|
}
|
|
288
322
|
packs.push(pack)
|
|
@@ -300,7 +334,9 @@ export default function Laws({ selected, overview, onAction }) {
|
|
|
300
334
|
})
|
|
301
335
|
}
|
|
302
336
|
const activePacks = packs.filter((p) => p.active)
|
|
303
|
-
|
|
337
|
+
// Rule by rule, not pack by pack: an active pack can carry an excluded rule, and this
|
|
338
|
+
// readout claims to say how many rules bind the install.
|
|
339
|
+
const ruleCount = packs.reduce((n, p) => n + p.rules.filter((r) => !r.off).length, 0)
|
|
304
340
|
|
|
305
341
|
// ---- the pack selection: STAGED here, applied once -------------------------------------
|
|
306
342
|
//
|
|
@@ -314,32 +350,62 @@ export default function Laws({ selected, overview, onAction }) {
|
|
|
314
350
|
// them describing a state nobody asked for, each writing a different `Active packs:` marker
|
|
315
351
|
// for the next reader to parse. So the switches edit local state and `Apply` sends the whole
|
|
316
352
|
// selection. The state itself lives above the early returns — see the ⚠ there.
|
|
317
|
-
const isOn = (
|
|
318
|
-
const
|
|
319
|
-
setPicked((cur) => (cur.includes(
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
const
|
|
353
|
+
const isOn = (addr) => picked.includes(addr)
|
|
354
|
+
const toggleRule = (addr) =>
|
|
355
|
+
setPicked((cur) => (cur.includes(addr) ? cur.filter((a) => a !== addr) : [...cur, addr]))
|
|
356
|
+
// A pack is never picked directly — it is on when any of its rules is. One source, so the
|
|
357
|
+
// header and the switches under it cannot disagree.
|
|
358
|
+
const packOn = (p) => p.rules.some((r) => isOn(r.addr))
|
|
359
|
+
// ⚠ A STAGED PACK MUST NOT READ AS A DEPLOYED ONE. `p.active` is what the install built;
|
|
360
|
+
// `packOn(p)` is what the switches currently say. Where they differ the header says so —
|
|
361
|
+
// reporting the staged state as fact is the one lie a control like this can tell.
|
|
362
|
+
const packState = (p) => {
|
|
363
|
+
const on = p.rules.filter((r) => isOn(r.addr)).length
|
|
364
|
+
if (!on) return p.active ? 'staged off' : 'not built in'
|
|
365
|
+
if (!p.active) return `staged on · ${on} of ${p.rules.length}`
|
|
366
|
+
return `${on} of ${p.rules.length} active`
|
|
367
|
+
}
|
|
368
|
+
// What the install excludes today: a rule that is off inside a pack it still builds in.
|
|
369
|
+
// A rule whose whole pack is absent is not an exclusion — the pack's absence says it, and
|
|
370
|
+
// naming it beside a `--doctrines` that omits the pack would be noise the CLI rejects.
|
|
371
|
+
const deployedExcluded = packs
|
|
372
|
+
.filter((p) => p.active)
|
|
373
|
+
.flatMap((p) => p.rules.filter((r) => r.off).map((r) => r.addr))
|
|
374
|
+
const allRules = packs.flatMap((p) => p.rules.map((r) => r.addr))
|
|
375
|
+
const pickedRules = allRules.filter(isOn)
|
|
376
|
+
const dirty = pickedRules.join(',') !== deployedKey
|
|
377
|
+
const CONSENT = 'process.5'
|
|
378
|
+
const losingConsent = deployedRules.includes(CONSENT) && !picked.includes(CONSENT)
|
|
323
379
|
// Nothing to rebuild means nothing to toggle — a source render with no deployed install
|
|
324
380
|
// still READS, it just cannot be changed from here.
|
|
325
381
|
const install = overview?.install
|
|
326
382
|
const canApply = Boolean(install && onAction)
|
|
327
383
|
|
|
384
|
+
// ⚠ TWO AXES OUT OF ONE SELECTION. The user only ever touches rules; the request carries
|
|
385
|
+
// both `doctrines` (the packs with at least one rule left) and `excludeRules` (the rules
|
|
386
|
+
// dropped from the packs that survive). An exclusion naming a pack that is already absent
|
|
387
|
+
// would be noise, so it is not sent — the pack's absence already says it.
|
|
328
388
|
const applyPacks = () => {
|
|
329
389
|
if (!canApply || !dirty) return
|
|
390
|
+
const keptPacks = packs.filter((p) => p.rules.some((r) => isOn(r.addr))).map((p) => p.pack)
|
|
391
|
+
const excludeRules = packs
|
|
392
|
+
.filter((p) => keptPacks.includes(p.pack))
|
|
393
|
+
.flatMap((p) => p.rules.filter((r) => !isOn(r.addr)).map((r) => r.addr))
|
|
330
394
|
const warn = losingConsent
|
|
331
|
-
? '\n\n⚠
|
|
332
|
-
'
|
|
395
|
+
? '\n\n⚠ process 5 carries commit/push consent. Dropping it also removes the git-gate ' +
|
|
396
|
+
'hook, so commits and pushes stop being confirmed at the tool boundary. ' +
|
|
333
397
|
'(rm -rf and force-push stay gated — those are Rule IV’s, not the pack’s.)'
|
|
334
398
|
: ''
|
|
399
|
+
const what = pickedRules.length
|
|
400
|
+
? `${pickedRules.length} of ${allRules.length} doctrine rules`
|
|
401
|
+
: 'no doctrine rules'
|
|
335
402
|
if (
|
|
336
403
|
window.confirm(
|
|
337
|
-
`Rebuild ${install.host} · ${install.scope} with ${
|
|
338
|
-
|
|
339
|
-
}? It rebuilds in place, non-destructive.${warn}`,
|
|
404
|
+
`Rebuild ${install.host} · ${install.scope} with ${what}? ` +
|
|
405
|
+
`It rebuilds in place, non-destructive.${warn}`,
|
|
340
406
|
)
|
|
341
407
|
)
|
|
342
|
-
onAction('install', { ...install, doctrines:
|
|
408
|
+
onAction('install', { ...install, doctrines: keptPacks, excludeRules })
|
|
343
409
|
}
|
|
344
410
|
|
|
345
411
|
const counts = {}
|
|
@@ -443,41 +509,18 @@ export default function Laws({ selected, overview, onAction }) {
|
|
|
443
509
|
</div>
|
|
444
510
|
{packs.map((p) => (
|
|
445
511
|
<div
|
|
446
|
-
className={`card law-wrap mb-16 pack-wrap ${
|
|
512
|
+
className={`card law-wrap mb-16 pack-wrap ${packOn(p) ? '' : 'pack-off'}`}
|
|
447
513
|
key={p.pack}
|
|
448
514
|
>
|
|
449
515
|
<div className="pack-head">
|
|
450
|
-
{canApply && (
|
|
451
|
-
<span
|
|
452
|
-
className={`sw-toggle${isOn(p.pack) ? ' on' : ''}`}
|
|
453
|
-
role="switch"
|
|
454
|
-
aria-checked={isOn(p.pack)}
|
|
455
|
-
aria-label={`${p.title} pack`}
|
|
456
|
-
tabIndex={0}
|
|
457
|
-
onClick={() => togglePack(p.pack)}
|
|
458
|
-
onKeyDown={(e) => {
|
|
459
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
460
|
-
if (e.key === ' ') e.preventDefault()
|
|
461
|
-
togglePack(p.pack)
|
|
462
|
-
}
|
|
463
|
-
}}
|
|
464
|
-
/>
|
|
465
|
-
)}
|
|
466
516
|
<span className="pack-name" style={{ '--cc': PACK_CATS[p.pack] }}>
|
|
467
517
|
<span className="cdot" />
|
|
468
518
|
{p.title}
|
|
469
519
|
</span>
|
|
470
520
|
<span className="pack-desc">{p.desc}</span>
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
? p.active
|
|
475
|
-
? 'active'
|
|
476
|
-
: 'not built in'
|
|
477
|
-
: isOn(p.pack)
|
|
478
|
-
? 'staged on'
|
|
479
|
-
: 'staged off'}
|
|
480
|
-
</span>
|
|
521
|
+
{/* Derived from the rules, never held separately — a pack IS however many of its
|
|
522
|
+
rules are on, and a second source could disagree with the switches above it. */}
|
|
523
|
+
<span className="pack-state">{packState(p)}</span>
|
|
481
524
|
</div>
|
|
482
525
|
{!p.active && !canApply && (
|
|
483
526
|
// The fallback for a console with no install to rebuild — a reader still needs the
|
|
@@ -485,7 +528,12 @@ export default function Laws({ selected, overview, onAction }) {
|
|
|
485
528
|
// ⚠ `geneseed-build`, not `geneseed build`: the CLI's `build` verb forwards
|
|
486
529
|
// `--theme` and nothing else, so the shorter spelling errors.
|
|
487
530
|
<div className="pack-enable">
|
|
488
|
-
$ geneseed-build --doctrines
|
|
531
|
+
$ geneseed-build --doctrines{' '}
|
|
532
|
+
{packs
|
|
533
|
+
.filter((q) => q.active || q.pack === p.pack)
|
|
534
|
+
.map((q) => q.pack)
|
|
535
|
+
.join(',')}
|
|
536
|
+
{deployedExcluded.length > 0 && ` --exclude-rules ${deployedExcluded.join(',')}`}
|
|
489
537
|
</div>
|
|
490
538
|
)}
|
|
491
539
|
<div className="law-rowhead">
|
|
@@ -493,9 +541,27 @@ export default function Laws({ selected, overview, onAction }) {
|
|
|
493
541
|
<span>Rule</span>
|
|
494
542
|
<span>Principle</span>
|
|
495
543
|
<span>Pack</span>
|
|
544
|
+
<span className="toggle-col">Toggle</span>
|
|
496
545
|
</div>
|
|
497
546
|
{p.rules.map((r) => (
|
|
498
|
-
<LawRow
|
|
547
|
+
<LawRow
|
|
548
|
+
key={r.addr}
|
|
549
|
+
law={{ ...r, off: !isOn(r.addr) }}
|
|
550
|
+
isOpen={open === r.addr}
|
|
551
|
+
onToggle={() => toggle(r.addr)}
|
|
552
|
+
toggleCol={
|
|
553
|
+
canApply ? (
|
|
554
|
+
<button
|
|
555
|
+
type="button"
|
|
556
|
+
className={`sw-toggle${isOn(r.addr) ? ' on' : ''}`}
|
|
557
|
+
role="switch"
|
|
558
|
+
aria-checked={isOn(r.addr)}
|
|
559
|
+
aria-label={`${r.name} rule`}
|
|
560
|
+
onClick={() => toggleRule(r.addr)}
|
|
561
|
+
/>
|
|
562
|
+
) : null
|
|
563
|
+
}
|
|
564
|
+
/>
|
|
499
565
|
))}
|
|
500
566
|
</div>
|
|
501
567
|
))}
|
|
@@ -503,14 +569,14 @@ export default function Laws({ selected, overview, onAction }) {
|
|
|
503
569
|
<div className="card pad-lg mb-16">
|
|
504
570
|
{losingConsent && (
|
|
505
571
|
<p className="pack-warn" role="status" aria-live="polite">
|
|
506
|
-
⚠ Dropping <b>process</b> also removes the commit/push consent gate —{' '}
|
|
572
|
+
⚠ Dropping <b>process 5</b> also removes the commit/push consent gate —{' '}
|
|
507
573
|
<code>git commit</code> and <code>git push</code> stop being confirmed at the tool
|
|
508
574
|
boundary. <code>rm -rf</code> and force-push stay gated.
|
|
509
575
|
</p>
|
|
510
576
|
)}
|
|
511
577
|
<div className="pack-apply">
|
|
512
578
|
<button className="btn soft" onClick={applyPacks} disabled={!dirty}>
|
|
513
|
-
Apply{dirty ? ` (${
|
|
579
|
+
Apply{dirty ? ` (${pickedRules.length}/${allRules.length} rules)` : ''}
|
|
514
580
|
</button>
|
|
515
581
|
<button
|
|
516
582
|
className="btn ghost"
|
package/docs/declined.md
DELETED
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
# Declined divergences
|
|
2
|
-
|
|
3
|
-
Places where this CLI deliberately does **not** reproduce the Python reference it was ported
|
|
4
|
-
from, with the reason written down. A divergence that is not on this page is a bug; one that is,
|
|
5
|
-
is a decision.
|
|
6
|
-
|
|
7
|
-
The reference has been deleted, so "the reference does X" is no longer an argument anyone can
|
|
8
|
-
re-run. What is recorded in `tests/__snapshots__/` is the reference's answer, frozen; what is
|
|
9
|
-
listed here is where this implementation answers differently on purpose.
|
|
10
|
-
|
|
11
|
-
**The recording is smaller than it was.** On **2026-08-17** the emit, cli and web corpora were
|
|
12
|
-
retired — 1381 cells, produced by an implementation that no longer exists and re-recordable by
|
|
13
|
-
nothing, so they reddened on every deliberate content change while saying nothing about a defect
|
|
14
|
-
that happened to be stable. What licensed it was a measurement: all five mutations whose only
|
|
15
|
-
declared gate was a corpus replay are killed by `tests/unit/` alone. `docs/limits.md` carries the
|
|
16
|
-
full argument and what it cost. **The consequence for this page is narrow and worth stating: an
|
|
17
|
-
entry whose "how it stays honest" half named one of those three corpora now names a weaker gate, or
|
|
18
|
-
none.** The help fixtures under `tests/__snapshots__/help/`, which most of this page rests on, are
|
|
19
|
-
untouched.
|
|
20
|
-
|
|
21
|
-
---
|
|
22
|
-
|
|
23
|
-
## `--help`: the prog is `geneseed`, not `harness`
|
|
24
|
-
|
|
25
|
-
**Status:** declined (the port's wording is kept), narrow and gated.
|
|
26
|
-
|
|
27
|
-
**What differs.** The reference opened `usage: harness status` for `status --help`; this CLI
|
|
28
|
-
opens `usage: geneseed status`. The same substitution applies to `bin/geneseed-hook.mjs`, which
|
|
29
|
-
spells itself `geneseed-hook`.
|
|
30
|
-
|
|
31
|
-
**Why.** `harness` named the Python file this migration deleted. It was not a command anyone
|
|
32
|
-
could type, and now it names nothing at all. This CLI already spells itself `geneseed` in every
|
|
33
|
-
error it prints (`geneseed: error: …`, `bin/geneseed-cli.mjs`'s `die`), so reproducing `harness`
|
|
34
|
-
here would make the help text the only surface still pointing at the old name.
|
|
35
|
-
|
|
36
|
-
**How it stays honest.** `prog` is an ARGUMENT of `js/cli.mjs`'s `formatHelp`, not a constant.
|
|
37
|
-
`tests/snapshot/cli_help.test.mjs` replays all 26 recorded fixtures through it with the reference's own
|
|
38
|
-
prog and requires them byte for byte. The rename is the only thing that moves, and a separate
|
|
39
|
-
assertion pins the shipped spelling so a renderer that ignored `prog` could not pass both.
|
|
40
|
-
|
|
41
|
-
That the formatter agrees at every WIDTH — argparse's whole layout: usage assembly, the wrapped
|
|
42
|
-
continuation indent, the two-column help position, the wrap column — was established by a live
|
|
43
|
-
sweep over all 26 verbs at every `COLUMNS` from 20 to 200 while the reference still ran. It
|
|
44
|
-
retired with the reference, and it is the reason the fixtures can be trusted: the 26 are recorded
|
|
45
|
-
at ONE width (`COLUMNS=80`), and at that width two real divergences were invisible — a mutually
|
|
46
|
-
exclusive group emitted as one unsplittable usage part, and `_max_help_position` baked as a
|
|
47
|
-
constant where argparse derives it from the width. Both were found by the sweep and fixed. A
|
|
48
|
-
claim about "the entire layout" that rests on a corpus at one width is a claim about one width,
|
|
49
|
-
so a change to the wrapping arithmetic now needs its own argument rather than a green corpus.
|
|
50
|
-
|
|
51
|
-
Note that the divergence could not have been applied to a recorded text after the fact: the
|
|
52
|
-
continuation indent of a wrapped usage line is `len('usage: ') + len(prog) + 1`, so four of the
|
|
53
|
-
26 usage blocks re-flow when the prog changes length.
|
|
54
|
-
|
|
55
|
-
---
|
|
56
|
-
|
|
57
|
-
## `update --help` names `update`, where the reference names `upgrade`
|
|
58
|
-
|
|
59
|
-
**Status:** reference behaviour, deliberately not reproduced.
|
|
60
|
-
|
|
61
|
-
**What differs.** The reference printed `usage: harness upgrade [-h] [theme]` for `update --help`
|
|
62
|
-
— the canonical verb, for a command the user spelled `update`. This CLI prints `usage: geneseed
|
|
63
|
-
update`.
|
|
64
|
-
|
|
65
|
-
**Why.** argparse implements an alias by registering a second key in `_SubParsersAction.choices`
|
|
66
|
-
pointing at the SAME parser object, and `parser.prog` was fixed when that object was built. The
|
|
67
|
-
leaked name is an implementation detail of the reference's alias mechanism, not a statement about
|
|
68
|
-
the CLI. Recorded verbatim in `tests/__snapshots__/help/update.txt` and replayed with the
|
|
69
|
-
reference's prog, so the reference's answer is preserved; the shipped call passes the verb the
|
|
70
|
-
user typed.
|
|
71
|
-
|
|
72
|
-
---
|
|
73
|
-
|
|
74
|
-
## `link` bakes the interpreter into a written shim, where the Unix arm used to symlink
|
|
75
|
-
|
|
76
|
-
**Status:** ACCEPTED, both consequences below. This is a change to the PRODUCT, not a port
|
|
77
|
-
divergence — since Task 2b both implementations write the same shim — and it is recorded here
|
|
78
|
-
because this is the page that exists for "what did we decide to live with".
|
|
79
|
-
|
|
80
|
-
**What changed.** `./geneseed link` used to symlink `ROOT/geneseed` into `~/.local/bin`; the
|
|
81
|
-
launcher then resolved the symlink at RUN TIME and found its own interpreter. It now writes a
|
|
82
|
-
`#!/bin/sh` shim carrying a `# GENESEED_LINK_SHIM` marker that `exec`s the runtime named by
|
|
83
|
-
absolute path (`process.execPath`), mirroring what the Windows arm has always done. The reason it
|
|
84
|
-
moved is in `.superpowers/sdd/progress.md` Task 2b: the two implementations disagreed on four
|
|
85
|
-
cells, and this repo has no deliberate-divergence mechanism, so the reference moved to the port's
|
|
86
|
-
shape rather than the corpus recording a split.
|
|
87
|
-
|
|
88
|
-
**Consequence 1 — durability.** The baked path can go away. A shim written under `nvm` names
|
|
89
|
-
`~/.nvm/versions/node/v22.x/bin/node`; switch or clean up, and `geneseed` on `PATH` points at a
|
|
90
|
-
runtime that no longer exists. The old symlink survived that, because it deferred the choice to the
|
|
91
|
-
launcher. Accepted: the fix is one `./geneseed link` re-run, the failure is loud and immediate
|
|
92
|
-
(`no such file or directory`), and the Windows arm has carried the same property since it
|
|
93
|
-
shipped. Prefer the old behaviour? A shell function in your rc does exactly that, and SETUP.md
|
|
94
|
-
offers it beside `link`.
|
|
95
|
-
|
|
96
|
-
**Consequence 2 — which CLI is on `PATH`.** The shim names the implementation that WROTE it, and
|
|
97
|
-
while two implementations existed that decided whether `geneseed tui` and `geneseed menu` opened
|
|
98
|
-
the full-screen panel or refused it. This is now settled rather than a consequence: there is one
|
|
99
|
-
implementation, and the panel it does not carry no longer exists anywhere to be reached. Both
|
|
100
|
-
verbs cross and both refuse the panel by name, stating what this entry does carry (the TUI's
|
|
101
|
-
layout half, not its screens) and offering `setup`, `doctor` and `build` instead. The refusal
|
|
102
|
-
names no file, deliberately — an earlier one pointed at the panel's source, and a pointer to
|
|
103
|
-
something a migration is deleting is worse than a plain statement that the screen is not there.
|
|
104
|
-
|
|
105
|
-
---
|
|
106
|
-
|
|
107
|
-
## argparse's ERROR wording is not reproduced
|
|
108
|
-
|
|
109
|
-
**Status:** declined, pre-existing (P5a, P10c), restated here because it is the neighbour of the
|
|
110
|
-
entry above and readers keep finding it separately.
|
|
111
|
-
|
|
112
|
-
argparse printed a usage block around every parse failure and prefixed it `harness: error:`; both
|
|
113
|
-
entries state the fault on one line prefixed `geneseed: error:` / `geneseed-hook: error:`. See
|
|
114
|
-
`bin/geneseed-cli.mjs`'s `parse` docblock.
|
|
115
|
-
|
|
116
|
-
**What used to be gated beside it no longer is.** Every error a command's own body raises was in
|
|
117
|
-
the recorded CLI corpus, replayed cell by cell; that corpus was retired on 2026-08-17 and the
|
|
118
|
-
per-verb error texts are now covered only where `tests/unit/` happens to drive the verb. The
|
|
119
|
-
divergence declared here is unchanged — the port has never reproduced argparse's wording and does
|
|
120
|
-
not start now — but the *neighbouring* claim, that the rest of the error surface is pinned byte for
|
|
121
|
-
byte, has expired.
|
|
122
|
-
|
|
123
|
-
---
|
|
124
|
-
|
|
125
|
-
## Top-level `--help` is still a refusal on the Node entries
|
|
126
|
-
|
|
127
|
-
**Status:** open, out of scope for Task 10, stated so it is not mistaken for done.
|
|
128
|
-
|
|
129
|
-
`geneseed --help` with no verb prints
|
|
130
|
-
`geneseed: error: the following arguments are required: cmd (one of …)` on stderr and exits 2,
|
|
131
|
-
where the reference printed the whole parser's help with a subcommand table. Task 10's surface
|
|
132
|
-
was the 26 per-verb help texts and only those are recorded.
|
|
133
|
-
|
|
134
|
-
Rendering the top-level help needs argparse's `_iter_indented_subactions` path — the subcommand
|
|
135
|
-
choices as a metavar in the usage line, and a nested, differently-indented action list whose
|
|
136
|
-
invocations feed the same `_action_max_length` — which is a distinct piece of the formatter with
|
|
137
|
-
no fixture behind it. Whoever writes it should record it the same way first.
|
|
138
|
-
|
|
139
|
-
---
|
|
140
|
-
|
|
141
|
-
## `--help` anywhere on the line wins, where argparse would fail on an earlier option
|
|
142
|
-
|
|
143
|
-
**Status:** declined (the port's behaviour is kept), and the gate that pins it has been corrected
|
|
144
|
-
to stop calling it argparse's rule.
|
|
145
|
-
|
|
146
|
-
**What differs.** Both Node entries scan the whole argument list for `-h`/`--help` BEFORE parsing
|
|
147
|
-
(`bin/geneseed-cli.mjs`, `bin/geneseed-hook.mjs`), so `geneseed version --target --help` prints
|
|
148
|
-
the help and exits 0. argparse consumes optionals left to right, so `--target` swallows `--help`
|
|
149
|
-
as its value and the reference exits 2 with `argument --target: expected one argument`. Three
|
|
150
|
-
error classes reach it — a missing value, an invalid `type=int` value (`web --port abc -h`), and a
|
|
151
|
-
mutex conflict (`theme --solid-only --transparent-only --help`) — across 19 mandatory-value
|
|
152
|
-
options on 13 verbs, plus `--memory` and `--root` on the hook entry.
|
|
153
|
-
|
|
154
|
-
**Why declined.** On a malformed command line the two answers are "help, exit 0" and "the error,
|
|
155
|
-
exit 2". Neither takes an action; nothing is written, nothing is destroyed. A scan that stopped at
|
|
156
|
-
the first option expecting a value would have to reproduce argparse's consumption order to be
|
|
157
|
-
right, and that order is the piece of the parser this port deliberately does not carry (see
|
|
158
|
-
argparse's ERROR wording above). Printing help for a line that contains `--help` is the answer a
|
|
159
|
-
user is more likely to have wanted, and it is the same answer for every shape of malformed line.
|
|
160
|
-
|
|
161
|
-
**How it stays honest.** `tests/snapshot/cli_help.test.mjs`'s `a --help anywhere in the line wins` used to
|
|
162
|
-
docblock this as "argparse's own rule". It is not — that generalised from `diff --nope --help`,
|
|
163
|
-
which works only because an UNKNOWN option is deferred to the end of `parse_known_args`. Two of
|
|
164
|
-
that test's four cases are cases where the reference errors instead, so the test pins a
|
|
165
|
-
divergence; its docblock now says which cases those are and points here.
|
|
166
|
-
|
|
167
|
-
## A verb that postdates the recorder is exempted from the cell matrix by name
|
|
168
|
-
|
|
169
|
-
**Status:** ACCEPTED, 2026-08-19. This widens an existing exemption rather than minting a second
|
|
170
|
-
one, and it changes a TEST, not the product.
|
|
171
|
-
|
|
172
|
-
**What differs.** `tests/unit/hook_cli.test.mjs`'s `the matrix covers every verb each entry
|
|
173
|
-
claims` asserted a strict equality between the hook verbs `tests/helpers/matrix/cli.*.json`
|
|
174
|
-
covers and the verbs `bin/geneseed-hook.mjs` carries. The CLI half of the same test already
|
|
175
|
-
exempted a named `NATIVE` list; the hook half exempted nothing. Both halves now filter through
|
|
176
|
-
the same list.
|
|
177
|
-
|
|
178
|
-
**Why the recording could not be kept as the whole answer.** The matrix was exported by the
|
|
179
|
-
reference implementation, which was deleted on 2026-08-17 along with every recorder that could
|
|
180
|
-
write a cell. So the covered set is frozen at four hook verbs for good. A fifth hook verb —
|
|
181
|
-
wired in `js/settings.mjs`, carried in `bin/geneseed-hook.mjs`, declared in `js/cli-table.json` —
|
|
182
|
-
would fail this equality with **no green path**: the corpus cannot grow, and the failure message
|
|
183
|
-
correctly forbids deleting cells to pass. The gate had become a wall, and a wall that stops a
|
|
184
|
-
legitimate change is not protection, it is an accident of when the recorder died.
|
|
185
|
-
|
|
186
|
-
The reason the three CLI verbs were exempted in the first place — *the reference had nothing to
|
|
187
|
-
compare against, and authoring one would be recording a copy of the value under test* — is
|
|
188
|
-
exactly the reason a verb that postdates the reference has no cell. Same reason, same exemption.
|
|
189
|
-
|
|
190
|
-
**What was lost, measured.** Nothing that another gate does not already hold. The only property
|
|
191
|
-
the frozen hook half carried beyond `NATIVE` was *"a hook verb cannot be removed by a coordinated
|
|
192
|
-
edit of three files"* — and `tests/unit/hook_cli.test.mjs`'s own literal (`sorted(wired)` against
|
|
193
|
-
the four names) plus its `verbsOf(HOOK) === wired` equality are two editable copies of that same
|
|
194
|
-
set, twelve lines above. `tests/mutate.mjs`'s M8 plants the one-file removal and is killed there,
|
|
195
|
-
not here.
|
|
196
|
-
|
|
197
|
-
**How it stays honest.** The exemption is a NAMED list, not a containment: a new verb has to be
|
|
198
|
-
written into `NATIVE` deliberately, in two files that read each other's source, and the reverse
|
|
199
|
-
check refuses an exemption for a verb neither entry point carries. So the next new verb still
|
|
200
|
-
fails loudly until someone decides about it — which is the whole point of the original design.
|