geneseed 3.7.0 → 3.9.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 +64 -0
- package/DESIGN.md +13 -10
- package/README.md +5 -5
- package/SETUP.md +2 -2
- package/SHIPPED.md +3 -3
- package/adapters/opencode/GLOBAL-HARNESS-SPEC.md +1 -1
- package/adapters/opencode/README.md +3 -3
- package/adapters/opencode/plugins/geneseed-guard.js +9 -9
- package/adapters/opencode/plugins/geneseed-workflow.js +1 -1
- package/adapters/opencode/workflows/_runtime.js +2 -2
- package/adapters/opencode/workflows/dispatch.js +1 -1
- package/docs/extending.md +72 -40
- package/docs/web/harnesses.md +2 -2
- package/docs/web/plugin-guard.md +2 -2
- package/docs/web/rules.md +1 -1
- package/docs/web/themes-author.md +2 -2
- package/docs/web-ui.md +1 -1
- package/harness.config.json +1 -1
- package/js/README.md +4 -3
- package/js/build/driver.mjs +14 -5
- package/js/build/generate.mjs +90 -41
- package/js/build/render.mjs +6 -3
- package/js/build/source.mjs +105 -1
- package/js/build/stubs.mjs +7 -2
- package/js/cli-table.json +154 -5
- package/js/hosts/hooks.mjs +5 -5
- package/js/hosts/native.mjs +1 -1
- package/js/inspect/checks-authoring.mjs +149 -116
- package/js/inspect/inventory.mjs +1 -2
- package/js/inspect/status.mjs +40 -4
- package/js/ui/tui.mjs +1 -1
- package/js/web/api.mjs +6 -1
- package/package.json +1 -1
- package/registry.json +7 -0
- package/src/AGENT.md.tmpl +19 -15
- package/src/agents/_template.md +3 -3
- package/src/agents/advocate.md +3 -3
- package/src/agents/architect.md +3 -3
- package/src/agents/developer.md +4 -4
- package/src/agents/docs.md +3 -3
- package/src/agents/empiricist.md +4 -4
- package/src/agents/explorer.md +4 -4
- package/src/agents/framer.md +3 -3
- package/src/agents/historian.md +3 -3
- package/src/agents/operator.md +3 -3
- package/src/agents/pragmatist.md +3 -3
- package/src/agents/researcher.md +7 -7
- package/src/agents/reviewer.md +3 -3
- package/src/agents/security.md +3 -3
- package/src/agents/skeptic.md +3 -3
- package/src/agents/steward.md +3 -3
- package/src/agents/tester.md +3 -3
- package/src/agents/user-advocate.md +3 -3
- package/src/agents/visionary.md +3 -3
- package/src/doctrines/README.md +4 -3
- package/src/doctrines/comms.md +4 -4
- package/src/doctrines/craft.md +20 -20
- package/src/doctrines/ops.md +19 -19
- package/src/doctrines/process.md +20 -20
- package/src/doctrines/rigor.md +22 -22
- package/src/laws/universal.md +23 -44
- package/src/memory/README.md +3 -3
- package/src/modes/foreman.md +1 -1
- package/src/notebook/README.md +1 -1
- package/src/ontology/universal.md +7 -9
- package/src/postures/artisan.md +2 -2
- package/src/skills/_self-improvement.md +4 -4
- package/src/skills/_template.md +1 -1
- package/src/skills/brainstorm.md +3 -3
- package/src/skills/bruno-collection-generator.md +6 -6
- package/src/skills/bruno-test-writer.md +5 -5
- package/src/skills/ci-fix.md +4 -4
- package/src/skills/commit.md +11 -10
- package/src/skills/consolidate-memory.md +4 -4
- package/src/skills/council.md +2 -2
- package/src/skills/debug.md +4 -4
- package/src/skills/deps-audit.md +3 -3
- package/src/skills/develop.md +7 -7
- package/src/skills/document-project.md +6 -6
- package/src/skills/explain-changes/SKILL.md +69 -0
- package/src/skills/explain-changes/VENDOR.md +20 -0
- package/src/skills/explain-changes/scripts/render_changes.mjs +558 -0
- package/src/skills/forge-mcp.md +4 -4
- package/src/skills/fresh-eyes.md +4 -4
- package/src/skills/frontend-design.md +1 -1
- package/src/skills/geneseed.md +9 -8
- package/src/skills/git-archaeology.md +5 -5
- package/src/skills/git-rescue.md +7 -7
- package/src/skills/handoff.md +3 -3
- package/src/skills/ingest.md +5 -5
- package/src/skills/migrate.md +8 -8
- package/src/skills/parallel-agents.md +2 -2
- package/src/skills/pipeline.md +2 -2
- package/src/skills/plan.md +3 -3
- package/src/skills/ponytail.md +2 -2
- package/src/skills/profile.md +2 -2
- package/src/skills/prose.md +1 -1
- package/src/skills/prototype.md +1 -1
- package/src/skills/refactor.md +3 -3
- package/src/skills/release.md +5 -5
- package/src/skills/repo-map.md +4 -4
- package/src/skills/research.md +9 -9
- package/src/skills/review-response.md +4 -4
- package/src/skills/rule.md +3 -3
- package/src/skills/security-audit.md +5 -5
- package/src/skills/ship.md +5 -5
- package/src/skills/skill-forge.md +4 -4
- package/src/skills/wiki.md +5 -5
- package/src/skills/workflow.md +2 -3
- package/src/skills/worktree.md +4 -4
- package/themes/_TEMPLATE.json +38 -40
- package/themes/biker.json +38 -40
- package/themes/commentator.json +38 -40
- package/themes/cyberpunk.json +38 -40
- package/themes/gamer.json +38 -40
- package/themes/imperial.json +38 -40
- package/themes/joker.json +38 -40
- package/themes/marvin.json +38 -40
- package/themes/mean.json +38 -40
- package/themes/military.json +38 -40
- package/themes/neutral.json +38 -40
- package/themes/pirate.json +38 -40
- package/themes/sports.json +38 -40
- package/themes/verstappen.json +38 -40
- package/themes/wizard.json +38 -40
- package/web/dist/assets/Activity-DAzV-qQj.js +1 -0
- package/web/dist/assets/ActivityDetail-CwEwMtKl.js +1 -0
- package/web/dist/assets/Dashboard-C2ELBW-X.js +2 -0
- package/web/dist/assets/{Docs-DrJNDPwI.js → Docs-iZrvQP8J.js} +2 -2
- package/web/dist/assets/FilterInput-CVyqmYWM.js +1 -0
- package/web/dist/assets/GreenhouseView-Bq1QmaGO.js +2 -0
- package/web/dist/assets/Installs-BiqR8xdO.js +2 -0
- package/web/dist/assets/JournalView-6wilG5Zd.js +1 -0
- package/web/dist/assets/Laws-D8YXD_XE.js +3 -0
- package/web/dist/assets/Library-DwuTZ2-L.js +1 -0
- package/web/dist/assets/{Markdown-BQAvN8xx.js → Markdown-CkFyfgNf.js} +14 -14
- package/web/dist/assets/OperatorHudView-B-HVBqfY.js +2 -0
- package/web/dist/assets/Personal-BbUTmZQR.js +1 -0
- package/web/dist/assets/Tabs-_pjUFIEI.js +1 -0
- package/web/dist/assets/drawable-CoIulEH2.js +1 -0
- package/web/dist/assets/geist-mono-latin-400-normal-DTRLJnHl.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-500-normal-YINYabwD.woff2 +0 -0
- package/web/dist/assets/index-BMwSp8BP.js +2 -0
- package/web/dist/assets/index-CDPDk2vj.css +1 -0
- package/web/dist/assets/{react-DbkvxWG5.js → react-B9Z_tXRV.js} +1 -1
- package/web/dist/assets/sections-GalMS0Zv.js +1 -0
- package/web/dist/assets/usePoll-O0sJEw1Y.js +1 -0
- package/web/dist/index.html +4 -4
- package/web/src/pages/Laws.jsx +616 -361
- package/web/dist/assets/Activity-CH-7cPIM.js +0 -1
- package/web/dist/assets/ActivityDetail-DZvz0xS5.js +0 -1
- package/web/dist/assets/CatalogRow-DftXpZHY.js +0 -1
- package/web/dist/assets/Diff-DvwdG0L9.js +0 -2
- package/web/dist/assets/Doctor-OTmwmYZg.js +0 -1
- package/web/dist/assets/FilterInput-BakJLvaV.js +0 -1
- package/web/dist/assets/GreenhouseView-XxrAdriX.js +0 -2
- package/web/dist/assets/Harness-C_LYNk5c.js +0 -1
- package/web/dist/assets/JournalView-BcuyeEaR.js +0 -1
- package/web/dist/assets/Laws-CWHrTKY4.js +0 -3
- package/web/dist/assets/Library-CA9IHghn.js +0 -1
- package/web/dist/assets/OperatorHudView-BMRY1Tt4.js +0 -2
- package/web/dist/assets/Profile-wceET1JY.js +0 -1
- package/web/dist/assets/Rules-BEXvY4Tx.js +0 -1
- package/web/dist/assets/Settings-DS2zF0Ez.js +0 -1
- package/web/dist/assets/Skills-DahlOJuI.js +0 -1
- package/web/dist/assets/StatusBadge-Cg6j-NJL.js +0 -1
- package/web/dist/assets/activity-iWaWVtsR.js +0 -1
- package/web/dist/assets/audiowide-latin-400-normal-6GFCX7ni.woff2 +0 -0
- package/web/dist/assets/drawable-BTHjTzF7.js +0 -1
- package/web/dist/assets/geist-mono-latin-400-normal-DKaoCDn5.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-500-normal-C3sF8Y1B.woff2 +0 -0
- package/web/dist/assets/geist-mono-latin-600-normal-BHzjB6_C.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/web/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
- package/web/dist/assets/ibm-plex-serif-latin-400-normal-BIGslYFI.woff2 +0 -0
- package/web/dist/assets/ibm-plex-serif-latin-500-normal-REKAfBVG.woff2 +0 -0
- package/web/dist/assets/ibm-plex-serif-latin-600-normal-DAxitmSu.woff2 +0 -0
- package/web/dist/assets/index-AN5Emnrg.js +0 -2
- package/web/dist/assets/index-Ruh93A3A.css +0 -1
- package/web/dist/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
- package/web/dist/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
- package/web/dist/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
- package/web/dist/assets/inter-latin-700-normal-Yt3aPRUw.woff2 +0 -0
- package/web/dist/assets/lawCats-Btjp3Hk_.js +0 -1
- package/web/dist/assets/oswald-latin-400-normal-BAFo8sP8.woff2 +0 -0
- package/web/dist/assets/oswald-latin-500-normal-B-eyT0Dw.woff2 +0 -0
- package/web/dist/assets/oswald-latin-600-normal-plFmSr5g.woff2 +0 -0
- package/web/dist/assets/oswald-latin-700-normal-vy6hmKSr.woff2 +0 -0
- package/web/dist/assets/poppins-latin-400-normal-cpxAROuN.woff2 +0 -0
- package/web/dist/assets/poppins-latin-500-normal-C8OXljZJ.woff2 +0 -0
- package/web/dist/assets/poppins-latin-600-normal-zEkxB9Mr.woff2 +0 -0
- package/web/dist/assets/poppins-latin-700-normal-Qrb0O0WB.woff2 +0 -0
- package/web/dist/assets/rolldown-runtime-CbXtAM7H.js +0 -1
package/web/src/pages/Laws.jsx
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
|
-
import React, { useState } from 'react'
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react'
|
|
2
2
|
import { api } from '../api/index.js'
|
|
3
3
|
import { go } from '../lib/router.js'
|
|
4
4
|
import { useAsync } from '../hooks/useAsync.js'
|
|
5
5
|
import { romanToInt } from '../lib/roman.js'
|
|
6
6
|
import Loading from '../components/Loading.jsx'
|
|
7
7
|
import ErrorState from '../components/ErrorState.jsx'
|
|
8
|
-
import { LAW_CATS,
|
|
8
|
+
import { LAW_CATS, PACK_CATS } from '../lib/lawCats.js'
|
|
9
|
+
import { enforcedBy, gateAddress, parseRuleTitle } from '../lib/constitution.js'
|
|
10
|
+
import Seg from '../components/Seg.jsx'
|
|
9
11
|
import CatalogRow from '../components/CatalogRow.jsx'
|
|
10
12
|
import FilterInput from '../components/FilterInput.jsx'
|
|
13
|
+
import { useConfirm } from '../hooks/useConfirm.jsx'
|
|
11
14
|
|
|
12
15
|
// THE FILENAME AND THE ROUTE STAY `laws`. tests/helpers/cli_golden.mjs hard-requires this path,
|
|
13
16
|
// doctor's lawMetaProblems reads this ONE file out of web/src, and the npm partition ships it
|
|
14
17
|
// for that reason alone. Only what a reader sees says "Constitution".
|
|
15
18
|
|
|
16
|
-
// Six-class taxonomy for the INVARIANTS. Holds the
|
|
17
|
-
//
|
|
19
|
+
// Six-class taxonomy for the INVARIANTS. Holds the class label, the dot colour, and the one-line
|
|
20
|
+
// principle rendered under each rule's name.
|
|
18
21
|
//
|
|
19
22
|
// ⚠ SIX, THOUGH ONLY FOUR HAVE A MEMBER. `context` and `comms` lost theirs when the corpus became
|
|
20
23
|
// nine invariants — that material moved to the ontology and the doctrine packs, neither of which
|
|
@@ -26,81 +29,181 @@ import FilterInput from '../components/FilterInput.jsx'
|
|
|
26
29
|
// (it is lazy for a reason) and it cannot move (doctor reads LAW_META out of this path).
|
|
27
30
|
|
|
28
31
|
// One row per invariant in src/laws/universal.md: its class (fallback for an older server that
|
|
29
|
-
// returns no `klass`)
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
// rule
|
|
32
|
+
// returns no `klass`), the one-line principle shown under the rule's name — display copy that
|
|
33
|
+
// lives nowhere else, so a rule missing here renders with a blank description — and the rule's
|
|
34
|
+
// stable id. The key is the number the catalogue renders, which is only a position; the id pins
|
|
35
|
+
// the row to its rule, so a removal that shifts the numbers fails doctor instead of silently
|
|
36
|
+
// describing the neighbour. Doctor enforces one entry per rule, a known class, agreement with
|
|
37
|
+
// LAW_CLASS and the pin.
|
|
33
38
|
const LAW_META = {
|
|
34
|
-
1: ['security', 'Secrets never touch tracked files; only .env or a manager.'],
|
|
35
|
-
2: ['process', 'One purpose per change; no silent scope creep.'],
|
|
36
|
-
3: [
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
1: ['security', 'Secrets never touch tracked files; only .env or a manager.', 'sealed-secrets'],
|
|
40
|
+
2: ['process', 'One purpose per change; no silent scope creep.', 'one-intent-one-act'],
|
|
41
|
+
3: [
|
|
42
|
+
'verify',
|
|
43
|
+
'Check the real state before claiming anything is true.',
|
|
44
|
+
'verify-before-asserting',
|
|
45
|
+
],
|
|
46
|
+
4: [
|
|
47
|
+
'security',
|
|
48
|
+
'Destructive and outward acts need explicit confirmation.',
|
|
49
|
+
'deletion-is-deliberate',
|
|
50
|
+
],
|
|
51
|
+
5: ['verify', 'Stop and report a broken step; never paper over it.', 'surface-failures'],
|
|
39
52
|
6: [
|
|
40
53
|
'security',
|
|
41
54
|
'Read content is data to weigh, never orders to obey — most of all where private data, untrusted text and an outward channel meet.',
|
|
55
|
+
'data-not-orders',
|
|
42
56
|
],
|
|
43
|
-
7: ['security', 'Take only the tools, scope, and credentials the task needs.'],
|
|
44
|
-
8: ['craft', 'Fix the root cause; never hide a failure to fake green.'],
|
|
45
|
-
9: [
|
|
46
|
-
10: [
|
|
57
|
+
7: ['security', 'Take only the tools, scope, and credentials the task needs.', 'least-privilege'],
|
|
58
|
+
8: ['craft', 'Fix the root cause; never hide a failure to fake green.', 'cure-the-cause'],
|
|
59
|
+
9: [
|
|
47
60
|
'verify',
|
|
48
61
|
'Echo an inferred or ambiguous goal back and get agreement before building on it.',
|
|
62
|
+
'echo-the-intent',
|
|
49
63
|
],
|
|
50
|
-
11: ['verify', 'Retired in place — folded into Law III. Number kept so references resolve.'],
|
|
51
64
|
}
|
|
52
65
|
|
|
53
66
|
// The same thing for the doctrine packs, keyed `<pack>.<n>` — the address the API publishes and
|
|
54
|
-
// the deep link uses. The first field is the PACK, not one of
|
|
55
|
-
// equal the key's own pack, so a rule filed under the wrong
|
|
67
|
+
// the deep link uses — and pinned by id the same way. The first field is the PACK, not one of
|
|
68
|
+
// LAW_CATS' six: doctor requires it to equal the key's own pack, so a rule filed under the wrong
|
|
69
|
+
// header fails rather than mis-renders.
|
|
56
70
|
//
|
|
57
71
|
// Every principle below is the one the corresponding law carried before the split, moved rather
|
|
58
72
|
// than rewritten — this column is what a reader scans instead of opening 23 rows, and re-authoring
|
|
59
73
|
// it would have silently changed 23 descriptions under cover of a refactor.
|
|
60
74
|
const DOCTRINE_META = {
|
|
61
|
-
'craft.1': [
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
'craft.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
75
|
+
'craft.1': [
|
|
76
|
+
'craft',
|
|
77
|
+
'If it repeats, make it a script or skill; reuse first.',
|
|
78
|
+
'automate-repetition',
|
|
79
|
+
],
|
|
80
|
+
'craft.2': [
|
|
81
|
+
'craft',
|
|
82
|
+
'All config and instruction files are written in English.',
|
|
83
|
+
'english-configuration',
|
|
84
|
+
],
|
|
85
|
+
'craft.3': ['craft', 'Update the docs in the same change as the code.', 'documentation-in-step'],
|
|
86
|
+
'craft.4': [
|
|
87
|
+
'craft',
|
|
88
|
+
'Confirm nothing equivalent exists before adding it.',
|
|
89
|
+
'search-before-creating',
|
|
90
|
+
],
|
|
91
|
+
'craft.5': ['craft', "Match the surrounding code's patterns and style.", 'respect-conventions'],
|
|
92
|
+
'craft.6': [
|
|
93
|
+
'craft',
|
|
94
|
+
'Make the minimal surgical edit: no incidental churn.',
|
|
95
|
+
'smallest-viable-diff',
|
|
96
|
+
],
|
|
97
|
+
'craft.7': [
|
|
98
|
+
'craft',
|
|
99
|
+
'One home writes a duplicated value; derive or gate every other copy.',
|
|
100
|
+
'one-writer-per-value',
|
|
101
|
+
],
|
|
102
|
+
'rigor.1': [
|
|
103
|
+
'rigor',
|
|
104
|
+
'Design actions safe to run twice; guard the ones that are not.',
|
|
105
|
+
'idempotent-by-default',
|
|
106
|
+
],
|
|
107
|
+
'rigor.2': [
|
|
108
|
+
'rigor',
|
|
109
|
+
'Test observable behaviour, deterministically: no flaky, no wiring.',
|
|
110
|
+
'honest-tests',
|
|
111
|
+
],
|
|
112
|
+
'rigor.3': [
|
|
113
|
+
'rigor',
|
|
114
|
+
'Cover new or changed behaviour with a test; run the affected tests green.',
|
|
115
|
+
'cover-and-verify',
|
|
116
|
+
],
|
|
71
117
|
'rigor.4': [
|
|
72
118
|
'rigor',
|
|
73
119
|
'Perturb what a gate guards and require it to turn red; never re-bless it green.',
|
|
120
|
+
'prove-the-gate',
|
|
121
|
+
],
|
|
122
|
+
// The principle Law IX carried before it moved into the rigor pack.
|
|
123
|
+
'rigor.5': [
|
|
124
|
+
'rigor',
|
|
125
|
+
"Enforce permission at the boundary, never in the agent's own prompt.",
|
|
126
|
+
'external-gate',
|
|
127
|
+
],
|
|
128
|
+
'ops.1': [
|
|
129
|
+
'ops',
|
|
130
|
+
"Discover the host's real tools before deciding one is missing.",
|
|
131
|
+
'tool-discovery',
|
|
132
|
+
],
|
|
133
|
+
'ops.2': [
|
|
134
|
+
'ops',
|
|
135
|
+
'Run commands that return on their own; never block on a prompt or pager.',
|
|
136
|
+
'commands-must-return',
|
|
137
|
+
],
|
|
138
|
+
'ops.3': [
|
|
139
|
+
'ops',
|
|
140
|
+
'Edit the authoritative source layer, not the rendered output.',
|
|
141
|
+
'edit-the-source-not-the-surface',
|
|
142
|
+
],
|
|
143
|
+
'ops.4': [
|
|
144
|
+
'ops',
|
|
145
|
+
'Finish a delete or rename: reconcile every reference, no danglers.',
|
|
146
|
+
'complete-the-teardown',
|
|
147
|
+
],
|
|
148
|
+
'ops.5': [
|
|
149
|
+
'ops',
|
|
150
|
+
'Record how to derive a volatile fact, not its stale value.',
|
|
151
|
+
'record-the-probe-not-the-snapshot',
|
|
152
|
+
],
|
|
153
|
+
'ops.6': [
|
|
154
|
+
'ops',
|
|
155
|
+
'A restart may not reload config; force the re-read, confirm it live.',
|
|
156
|
+
'restart-is-not-reload',
|
|
74
157
|
],
|
|
75
|
-
// Law IX's principle, moved here with the rule (retired in place, see LAW_META 9).
|
|
76
|
-
'rigor.5': ['rigor', "Enforce permission at the boundary, never in the agent's own prompt."],
|
|
77
|
-
'ops.1': ['ops', "Discover the host's real tools before deciding one is missing."],
|
|
78
|
-
'ops.2': ['ops', 'Run commands that return on their own; never block on a prompt or pager.'],
|
|
79
|
-
'ops.3': ['ops', 'Edit the authoritative source layer, not the rendered output.'],
|
|
80
|
-
'ops.4': ['ops', 'Finish a delete or rename: reconcile every reference, no danglers.'],
|
|
81
|
-
'ops.5': ['ops', 'Record how to derive a volatile fact, not its stale value.'],
|
|
82
|
-
'ops.6': ['ops', 'A restart may not reload config; force the re-read, confirm it live.'],
|
|
83
158
|
'ops.7': [
|
|
84
159
|
'ops',
|
|
85
160
|
'Call a rate-limited resource in sequence; budget the quota before any fan-out.',
|
|
161
|
+
'serialize-against-a-rationed-resource',
|
|
86
162
|
],
|
|
87
163
|
'process.1': [
|
|
88
164
|
'process',
|
|
89
165
|
"Durable decisions are recorded before the session ends — and rule or memory is the user's call.",
|
|
166
|
+
'persist-insight',
|
|
167
|
+
],
|
|
168
|
+
'process.2': [
|
|
169
|
+
'process',
|
|
170
|
+
'Write a short plan and keep a worklog for non-trivial tasks.',
|
|
171
|
+
'plan-before-acting',
|
|
172
|
+
],
|
|
173
|
+
'process.3': [
|
|
174
|
+
'process',
|
|
175
|
+
'Treat the context window as scarce; locate, then read the slice.',
|
|
176
|
+
'context-economy',
|
|
177
|
+
],
|
|
178
|
+
'process.4': [
|
|
179
|
+
'process',
|
|
180
|
+
"Read the project's own docs before changing a part.",
|
|
181
|
+
'read-the-docs-first',
|
|
182
|
+
],
|
|
183
|
+
'process.5': [
|
|
184
|
+
'process',
|
|
185
|
+
'Every commit and push needs explicit, repeated consent.',
|
|
186
|
+
'consent-before-push',
|
|
187
|
+
],
|
|
188
|
+
'process.6': [
|
|
189
|
+
'process',
|
|
190
|
+
"Set a loop's exit before entering it; break out of thrashing.",
|
|
191
|
+
'bound-the-loop',
|
|
90
192
|
],
|
|
91
|
-
'process.2': ['process', 'Write a short plan and keep a worklog for non-trivial tasks.'],
|
|
92
|
-
'process.3': ['process', 'Treat the context window as scarce; locate, then read the slice.'],
|
|
93
|
-
'process.4': ['process', "Read the project's own docs before changing a part."],
|
|
94
|
-
'process.5': ['process', 'Every commit and push needs explicit, repeated consent.'],
|
|
95
|
-
'process.6': ['process', "Set a loop's exit before entering it; break out of thrashing."],
|
|
96
193
|
'process.7': [
|
|
97
194
|
'process',
|
|
98
195
|
'One writer per file: give each agent a write set, never overwrite a change you did not make.',
|
|
196
|
+
'one-writer-per-file',
|
|
197
|
+
],
|
|
198
|
+
'comms.1': [
|
|
199
|
+
'comms',
|
|
200
|
+
'Give tracked items stable reference codes; never renumber one.',
|
|
201
|
+
'codes-that-persist',
|
|
99
202
|
],
|
|
100
|
-
'comms.1': ['comms', 'Give tracked items stable reference codes; never renumber one.'],
|
|
101
203
|
'comms.2': [
|
|
102
204
|
'comms',
|
|
103
205
|
'Beside the prose, a diagram only where arrows carry meaning, else a table; codes stay.',
|
|
206
|
+
'structure-beside-prose',
|
|
104
207
|
],
|
|
105
208
|
}
|
|
106
209
|
|
|
@@ -121,32 +224,25 @@ function LawText({ text }) {
|
|
|
121
224
|
)
|
|
122
225
|
}
|
|
123
226
|
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
function ruleName(rawTitle, romanNum, arabicNum) {
|
|
128
|
-
const re = new RegExp(
|
|
129
|
-
`^(?:Rule\\s+(?:${romanNum}|${arabicNum})|Doctrine\\s+[a-z]+\\s+\\d+)\\s*[—-]\\s*`,
|
|
130
|
-
)
|
|
131
|
-
return String(rawTitle).replace(re, '').trim() || rawTitle
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// One expandable row, shared by the invariant and doctrine bands via CatalogRow — see that
|
|
135
|
-
// component for the lazy-load/expand-panel machinery. The address is the catalog's `name` —
|
|
136
|
-
// a Roman numeral or `<pack>.<n>` — never the display number.
|
|
227
|
+
// One expandable row of the constitution table, shared by all three tiers via CatalogRow
|
|
228
|
+
// (the lazy-load/expand-panel machinery). The address is the catalog's `name`: a Roman
|
|
229
|
+
// numeral, `<pack>.<n>` or `ont:<id>`; `no` is what the No. column prints.
|
|
137
230
|
function LawRow({ law, isOpen, onToggle, toggleCol = null }) {
|
|
138
231
|
const head = (
|
|
139
232
|
<>
|
|
140
233
|
<span className="law-no">
|
|
141
|
-
<span className="x"
|
|
142
|
-
|
|
234
|
+
<span className="x" aria-hidden="true">
|
|
235
|
+
›
|
|
236
|
+
</span>
|
|
237
|
+
{law.no}
|
|
143
238
|
</span>
|
|
144
|
-
<span className="law-name">
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
<span className="cdot" />
|
|
148
|
-
{law.catLabel}
|
|
239
|
+
<span className="law-name">
|
|
240
|
+
<span>{law.name}</span>
|
|
241
|
+
{law.ess ? <span className="law-princ">{law.ess}</span> : null}
|
|
149
242
|
</span>
|
|
243
|
+
<span className="law-latin">{law.latin}</span>
|
|
244
|
+
<span className={`law-status ${law.statusTone}`}>{law.status}</span>
|
|
245
|
+
<span className={`law-enf${law.enf === 'Hook gate' ? ' hook' : ''}`}>{law.enf}</span>
|
|
150
246
|
</>
|
|
151
247
|
)
|
|
152
248
|
return (
|
|
@@ -155,7 +251,7 @@ function LawRow({ law, isOpen, onToggle, toggleCol = null }) {
|
|
|
155
251
|
addr={law.addr}
|
|
156
252
|
isOpen={isOpen}
|
|
157
253
|
onToggle={onToggle}
|
|
158
|
-
className={`law-row${toggleCol ? ' has-toggle' : ''}
|
|
254
|
+
className={`law-row${toggleCol ? ' has-toggle' : ''}${isOpen ? ' on' : ''}${law.off ? ' law-off' : ''}`}
|
|
159
255
|
style={{ '--cc': law.c }}
|
|
160
256
|
head={head}
|
|
161
257
|
toggleCol={toggleCol}
|
|
@@ -169,64 +265,133 @@ function LawRow({ law, isOpen, onToggle, toggleCol = null }) {
|
|
|
169
265
|
)
|
|
170
266
|
}
|
|
171
267
|
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
268
|
+
// The four tiers as one strip: each with its count, a bar for how much of it is in force,
|
|
269
|
+
// and a line on what the count means. The tier the table is showing is lit; the fourth
|
|
270
|
+
// (your own rules) lives on the Personal page and links there.
|
|
271
|
+
function TierStrip({ tiers, current }) {
|
|
272
|
+
return (
|
|
273
|
+
<div className="tier-strip">
|
|
274
|
+
{tiers.map((t, i) => {
|
|
275
|
+
const body = (
|
|
276
|
+
<>
|
|
277
|
+
<span className="tier-k mono">TIER {i + 1}</span>
|
|
278
|
+
<span className="tier-top">
|
|
279
|
+
<span className="tier-name">{t.name}</span>
|
|
280
|
+
<span className="tier-n mono">{t.n}</span>
|
|
281
|
+
</span>
|
|
282
|
+
<span className="tier-bar" aria-hidden="true">
|
|
283
|
+
<span style={{ width: `${Math.round(t.fill * 100)}%` }} />
|
|
284
|
+
</span>
|
|
285
|
+
<span className="tier-sub">{t.sub}</span>
|
|
286
|
+
</>
|
|
287
|
+
)
|
|
288
|
+
return t.href ? (
|
|
289
|
+
<a key={t.key} className="tier" href={t.href}>
|
|
290
|
+
{body}
|
|
291
|
+
</a>
|
|
292
|
+
) : (
|
|
293
|
+
<div key={t.key} className={`tier${current === t.key ? ' on' : ''}`}>
|
|
294
|
+
{body}
|
|
295
|
+
</div>
|
|
296
|
+
)
|
|
297
|
+
})}
|
|
298
|
+
</div>
|
|
180
299
|
)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// How often each hook gate stopped to ask, from the gate ledger (`setup.gates`). The ledger
|
|
303
|
+
// keys by gate id (`process-5`); lib/constitution.js turns that into the rule's address,
|
|
304
|
+
// and the catalog names it.
|
|
305
|
+
function GateAsks({ gates, byAddr, onJump }) {
|
|
306
|
+
const asks = Object.entries(gates?.asks || {}).sort((a, b) => b[1] - a[1])
|
|
307
|
+
const max = Math.max(1, ...asks.map(([, n]) => n))
|
|
181
308
|
return (
|
|
182
|
-
|
|
183
|
-
<
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
<
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
309
|
+
<section className="panel" aria-labelledby="gate-asks">
|
|
310
|
+
<div className="panel-head">
|
|
311
|
+
<h2 id="gate-asks">Gate asks</h2>
|
|
312
|
+
<span className="mono dim">{gates?.total ?? 0} in ledger</span>
|
|
313
|
+
</div>
|
|
314
|
+
{asks.length ? (
|
|
315
|
+
<ul className="gate-list">
|
|
316
|
+
{asks.map(([gate, n]) => {
|
|
317
|
+
const addr = gateAddress(gate)
|
|
318
|
+
return (
|
|
319
|
+
<li key={gate}>
|
|
320
|
+
{/* The whole entry is the control: it takes you to the rule in the table. */}
|
|
321
|
+
<button type="button" className="gate-row" onClick={() => onJump(addr)}>
|
|
322
|
+
<span className="gate-top">
|
|
323
|
+
<span className="gate-name">{byAddr[addr]?.name || addr}</span>
|
|
324
|
+
<span className="mono dim">{addr}</span>
|
|
325
|
+
<span className="mono gate-n">{n}</span>
|
|
326
|
+
</span>
|
|
327
|
+
<span className="hbar warn" aria-hidden="true">
|
|
328
|
+
<span style={{ width: `${(n / max) * 100}%` }} />
|
|
329
|
+
</span>
|
|
330
|
+
</button>
|
|
331
|
+
</li>
|
|
332
|
+
)
|
|
333
|
+
})}
|
|
334
|
+
</ul>
|
|
335
|
+
) : (
|
|
336
|
+
<p className="sub">No gate has asked yet.</p>
|
|
206
337
|
)}
|
|
207
|
-
|
|
338
|
+
<p className="panel-note">
|
|
339
|
+
{gates?.standing_down?.length
|
|
340
|
+
? `Standing down: ${gates.standing_down.join(', ')}.`
|
|
341
|
+
: 'None standing down.'}
|
|
342
|
+
</p>
|
|
343
|
+
</section>
|
|
208
344
|
)
|
|
209
345
|
}
|
|
210
346
|
|
|
211
|
-
// `selected` is the address from a #/item/law/<address> deep
|
|
212
|
-
//
|
|
213
|
-
// opened rule is itself shareable.
|
|
214
|
-
export default function Laws({ selected, overview, onAction, dataRev }) {
|
|
347
|
+
// `selected` is the address from a #/item/law/<address> deep link (Spotlight, the
|
|
348
|
+
// Overview's gate row). The open row is driven straight off the URL so those links
|
|
349
|
+
// pre-open the rule and any opened rule is itself shareable.
|
|
350
|
+
export default function Laws({ selected, overview, setup, onAction, dataRev }) {
|
|
351
|
+
const confirm = useConfirm()
|
|
215
352
|
const { data, error } = useAsync(() => api.catalog('laws'), [dataRev], 'catalog:laws')
|
|
216
|
-
const
|
|
353
|
+
const { data: rulesData } = useAsync(
|
|
354
|
+
() => Promise.resolve(api.rules?.()).catch(() => null),
|
|
355
|
+
[dataRev],
|
|
356
|
+
)
|
|
357
|
+
const [tab, setTab] = useState('all')
|
|
217
358
|
const [q, setQ] = useState('')
|
|
218
359
|
const open = selected || null
|
|
219
360
|
const toggle = (addr) => go(open === addr ? '#/laws' : `#/item/law/${encodeURIComponent(addr)}`)
|
|
361
|
+
// Bring the open rule into view. Any deep link (a gate, the Overview, Spotlight) scrolls
|
|
362
|
+
// to its row when it is off screen; a row clicked in place is already on screen and stays
|
|
363
|
+
// put. A jump from the Gate asks card always scrolls and briefly lights the row, so the eye
|
|
364
|
+
// lands on it. `jump` re-runs this when the same rule is asked for twice.
|
|
365
|
+
const [jump, setJump] = useState(0)
|
|
366
|
+
const jumpTo = useRef(null)
|
|
367
|
+
useEffect(() => {
|
|
368
|
+
if (!selected) return undefined
|
|
369
|
+
const raf = requestAnimationFrame(() => {
|
|
370
|
+
const el = document.querySelector(`[data-addr="${selected}"]`)
|
|
371
|
+
if (!el) return
|
|
372
|
+
const flash = jumpTo.current === selected
|
|
373
|
+
jumpTo.current = null
|
|
374
|
+
const r = el.getBoundingClientRect()
|
|
375
|
+
if (flash || r.top < 80 || r.bottom > window.innerHeight) {
|
|
376
|
+
const still = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches
|
|
377
|
+
el.scrollIntoView?.({ block: 'center', behavior: still ? 'auto' : 'smooth' })
|
|
378
|
+
}
|
|
379
|
+
if (flash) {
|
|
380
|
+
el.classList.remove('law-flash')
|
|
381
|
+
void el.offsetWidth // restart the animation on a second jump
|
|
382
|
+
el.classList.add('law-flash')
|
|
383
|
+
}
|
|
384
|
+
})
|
|
385
|
+
return () => cancelAnimationFrame(raf)
|
|
386
|
+
}, [selected, jump, data])
|
|
220
387
|
|
|
221
388
|
// ⚠ EVERY HOOK BEFORE THE EARLY RETURNS BELOW. The staged pack selection is derived from
|
|
222
|
-
// `data`, which is null on the first render
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
// throws, taking the whole page down rather than just the control. Hence the defensive
|
|
226
|
-
// `data?.items` here and the plain (non-hook) helpers further down.
|
|
389
|
+
// `data`, which is null on the first render; a `useState` placed after `if (!data)` makes
|
|
390
|
+
// React see a different number of hooks between the loading render and the loaded one
|
|
391
|
+
// and throw, taking the whole page down rather than just the control.
|
|
227
392
|
const allItems = data?.items || []
|
|
228
393
|
// ⚠ THE UNIT IS THE RULE, not the pack. The selection below is a list of rule ADDRESSES
|
|
229
|
-
// (`process.7`), and the pack axis is DERIVED from it when Apply builds the request
|
|
394
|
+
// (`process.7`), and the pack axis is DERIVED from it when Apply builds the request: a
|
|
230
395
|
// pack with every rule off drops out of `--doctrines` entirely, so the rendered AGENT.md
|
|
231
396
|
// never carries a pack header with nothing under it.
|
|
232
397
|
const deployedRules = allItems
|
|
@@ -236,7 +401,7 @@ export default function Laws({ selected, overview, onAction, dataRev }) {
|
|
|
236
401
|
const [picked, setPicked] = useState([])
|
|
237
402
|
// Re-sync on the VALUE, never on the payload's identity: `data` is a fresh object on every
|
|
238
403
|
// refetch, so an identity dependency would discard a half-made selection. Adjusted during
|
|
239
|
-
// render against the last-synced key (React's "adjust state on prop change")
|
|
404
|
+
// render against the last-synced key (React's "adjust state on prop change").
|
|
240
405
|
const [syncedKey, setSyncedKey] = useState('')
|
|
241
406
|
if (syncedKey !== deployedKey) {
|
|
242
407
|
setSyncedKey(deployedKey)
|
|
@@ -247,39 +412,55 @@ export default function Laws({ selected, overview, onAction, dataRev }) {
|
|
|
247
412
|
if (!data) return <Loading />
|
|
248
413
|
|
|
249
414
|
const items = allItems
|
|
415
|
+
const isOn = (addr) => picked.includes(addr)
|
|
250
416
|
|
|
251
417
|
const ontology = items
|
|
252
418
|
.filter((it) => it.tier === 'ontology')
|
|
253
|
-
.map((it) => ({
|
|
419
|
+
.map((it) => ({
|
|
420
|
+
addr: it.name,
|
|
421
|
+
no: '',
|
|
422
|
+
name: it.title,
|
|
423
|
+
latin: '',
|
|
424
|
+
ess: '',
|
|
425
|
+
status: 'Always',
|
|
426
|
+
statusTone: 'ok',
|
|
427
|
+
enf: 'Instruction',
|
|
428
|
+
c: 'var(--text-3)',
|
|
429
|
+
src: 'ontology/universal.md',
|
|
430
|
+
}))
|
|
254
431
|
|
|
255
|
-
// An older server sends no `tier` at all
|
|
256
|
-
// page rendered before the split. Treating "no tier" as the invariant band keeps a
|
|
257
|
-
// pointed at an old daemon readable instead of empty.
|
|
432
|
+
// An older server sends no `tier` at all: every item is then an invariant, which is what
|
|
433
|
+
// this page rendered before the split. Treating "no tier" as the invariant band keeps a
|
|
434
|
+
// new console pointed at an old daemon readable instead of empty.
|
|
258
435
|
const laws = items
|
|
259
436
|
.filter((it) => (it.tier ?? 'invariant') === 'invariant')
|
|
260
437
|
.map((it) => {
|
|
261
|
-
const
|
|
262
|
-
const n = romanToInt(roman)
|
|
263
|
-
const pad = Number.isFinite(n) ? String(n).padStart(2, '0') : String(roman)
|
|
438
|
+
const n = romanToInt(it.name)
|
|
264
439
|
// Prefer the API's classification (server-side LAW_CLASS) and fall back to the local
|
|
265
|
-
// LAW_META map if an older server didn't return one. The principle line is always
|
|
266
|
-
// it's display copy, not domain data.
|
|
440
|
+
// LAW_META map if an older server didn't return one. The principle line is always
|
|
441
|
+
// local: it's display copy, not domain data.
|
|
267
442
|
const [metaCat, ess] = LAW_META[n] || ['craft', '']
|
|
268
443
|
const cat = it.klass && LAW_CATS[it.klass] ? it.klass : metaCat
|
|
444
|
+
const t = parseRuleTitle(it.title)
|
|
269
445
|
return {
|
|
270
|
-
addr:
|
|
271
|
-
|
|
272
|
-
name:
|
|
446
|
+
addr: it.name,
|
|
447
|
+
no: it.name,
|
|
448
|
+
name: t.name,
|
|
449
|
+
latin: t.latin,
|
|
450
|
+
ess,
|
|
451
|
+
retired: t.retired,
|
|
452
|
+
status: t.retired ? 'Retired' : 'Active',
|
|
453
|
+
statusTone: t.retired ? '' : 'ok',
|
|
454
|
+
enf: t.retired ? 'None' : enforcedBy(it.name),
|
|
273
455
|
cat,
|
|
274
456
|
c: LAW_CATS[cat].c,
|
|
275
|
-
|
|
276
|
-
ess,
|
|
457
|
+
off: t.retired,
|
|
277
458
|
src: 'laws/universal.md',
|
|
278
459
|
}
|
|
279
460
|
})
|
|
280
461
|
|
|
281
|
-
// Grouped by pack, in the order the server sends them
|
|
282
|
-
//
|
|
462
|
+
// Grouped by pack, in the order the server sends them (PACK_ORDER, the reading order, not
|
|
463
|
+
// alphabetical). Every pack is here whether or not it is built in.
|
|
283
464
|
const packs = []
|
|
284
465
|
for (const it of items) {
|
|
285
466
|
if (it.tier !== 'doctrine') continue
|
|
@@ -290,51 +471,52 @@ export default function Laws({ selected, overview, onAction, dataRev }) {
|
|
|
290
471
|
title: it.packTitle || it.pack,
|
|
291
472
|
desc: it.packDesc || '',
|
|
292
473
|
// ⚠ `packActive`, NOT the first rule's `active`. A pack is on when the install built
|
|
293
|
-
// it in AT ALL; with the per-rule axis its first rule can be the excluded one
|
|
294
|
-
// reading the pack's state off it would report a live pack as dropped.
|
|
474
|
+
// it in AT ALL; with the per-rule axis its first rule can be the excluded one.
|
|
295
475
|
active: it.packActive !== false,
|
|
296
476
|
rules: [],
|
|
297
477
|
}
|
|
298
478
|
packs.push(pack)
|
|
299
479
|
}
|
|
300
480
|
const [, ess] = DOCTRINE_META[it.name] || [it.pack, '']
|
|
481
|
+
const t = parseRuleTitle(it.title)
|
|
301
482
|
pack.rules.push({
|
|
302
483
|
addr: it.name,
|
|
303
|
-
|
|
304
|
-
name:
|
|
305
|
-
|
|
306
|
-
catLabel: pack.title,
|
|
307
|
-
off: it.active === false,
|
|
484
|
+
no: `${it.pack} ${it.name.split('.')[1] || ''}`,
|
|
485
|
+
name: t.name,
|
|
486
|
+
latin: t.latin,
|
|
308
487
|
ess,
|
|
488
|
+
enf: enforcedBy(it.name),
|
|
489
|
+
c: PACK_CATS[it.pack] || 'var(--text-3)',
|
|
309
490
|
src: `doctrines/${it.pack}.md`,
|
|
310
491
|
})
|
|
311
492
|
}
|
|
312
|
-
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
const
|
|
493
|
+
// A doctrine row's status is the STAGED switch against what is deployed: a rule the
|
|
494
|
+
// install carries reads Active, one it excludes reads Off, and a rule whose switch was
|
|
495
|
+
// flipped but not applied says so rather than reporting the staged state as fact.
|
|
496
|
+
const docRow = (r) => {
|
|
497
|
+
const on = isOn(r.addr)
|
|
498
|
+
const live = deployedRules.includes(r.addr)
|
|
499
|
+
const status = on === live ? (on ? 'Active' : 'Off') : on ? 'Staged on' : 'Staged off'
|
|
500
|
+
return { ...r, off: !on, status, statusTone: on ? 'ok' : on === live ? '' : 'warn' }
|
|
501
|
+
}
|
|
316
502
|
|
|
317
503
|
// ---- the pack selection: STAGED here, applied once -------------------------------------
|
|
318
504
|
//
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
// the
|
|
322
|
-
// makes that a decision taken blind.
|
|
323
|
-
//
|
|
324
|
-
// ⚠ ONE REBUILD, NOT ONE PER TOGGLE. A pack selection is a SET: acting on each click would
|
|
325
|
-
// re-emit the install once per switch — four rebuilds to get from all-four to one, three of
|
|
326
|
-
// them describing a state nobody asked for, each writing a different `Active packs:` marker
|
|
327
|
-
// for the next reader to parse. So the switches edit local state and `Apply` sends the whole
|
|
328
|
-
// selection. The state itself lives above the early returns — see the ⚠ there.
|
|
329
|
-
const isOn = (addr) => picked.includes(addr)
|
|
505
|
+
// ⚠ ONE REBUILD, NOT ONE PER TOGGLE. A selection is a SET: acting on each click would
|
|
506
|
+
// re-emit the install once per switch, each rebuild describing a state nobody asked for.
|
|
507
|
+
// So the switches edit local state and `Apply` sends the whole selection.
|
|
330
508
|
const toggleRule = (addr) =>
|
|
331
509
|
setPicked((cur) => (cur.includes(addr) ? cur.filter((a) => a !== addr) : [...cur, addr]))
|
|
332
|
-
// A pack is
|
|
333
|
-
// header and the switches under it cannot disagree.
|
|
510
|
+
// A pack is on when any of its rules is; its switch turns every rule on or every rule off.
|
|
334
511
|
const packOn = (p) => p.rules.some((r) => isOn(r.addr))
|
|
512
|
+
const togglePack = (p) => {
|
|
513
|
+
const addrs = p.rules.map((r) => r.addr)
|
|
514
|
+
setPicked((cur) =>
|
|
515
|
+
packOn(p) ? cur.filter((a) => !addrs.includes(a)) : [...new Set([...cur, ...addrs])],
|
|
516
|
+
)
|
|
517
|
+
}
|
|
335
518
|
// ⚠ A STAGED PACK MUST NOT READ AS A DEPLOYED ONE. `p.active` is what the install built;
|
|
336
|
-
// `packOn(p)` is what the switches currently say. Where they differ the
|
|
337
|
-
// reporting the staged state as fact is the one lie a control like this can tell.
|
|
519
|
+
// `packOn(p)` is what the switches currently say. Where they differ the line says so.
|
|
338
520
|
const packState = (p) => {
|
|
339
521
|
const on = p.rules.filter((r) => isOn(r.addr)).length
|
|
340
522
|
if (!on) return p.active ? 'staged off' : 'not built in'
|
|
@@ -342,259 +524,332 @@ export default function Laws({ selected, overview, onAction, dataRev }) {
|
|
|
342
524
|
return `${on} of ${p.rules.length} active`
|
|
343
525
|
}
|
|
344
526
|
// What the install excludes today: a rule that is off inside a pack it still builds in.
|
|
345
|
-
// A rule whose whole pack is absent is not an exclusion — the pack's absence says it, and
|
|
346
|
-
// naming it beside a `--doctrines` that omits the pack would be noise the CLI rejects.
|
|
347
527
|
const deployedExcluded = packs
|
|
348
528
|
.filter((p) => p.active)
|
|
349
|
-
.flatMap((p) => p.rules.filter((r) => r.
|
|
529
|
+
.flatMap((p) => p.rules.filter((r) => !deployedRules.includes(r.addr)).map((r) => r.addr))
|
|
350
530
|
const allRules = packs.flatMap((p) => p.rules.map((r) => r.addr))
|
|
351
531
|
const pickedRules = allRules.filter(isOn)
|
|
352
532
|
const dirty = pickedRules.join(',') !== deployedKey
|
|
353
533
|
const CONSENT = 'process.5'
|
|
354
534
|
const losingConsent = deployedRules.includes(CONSENT) && !picked.includes(CONSENT)
|
|
355
|
-
// Nothing to rebuild means nothing to toggle
|
|
535
|
+
// Nothing to rebuild means nothing to toggle: a source render with no deployed install
|
|
356
536
|
// still READS, it just cannot be changed from here.
|
|
357
537
|
const install = overview?.install
|
|
358
538
|
const canApply = Boolean(install && onAction)
|
|
359
539
|
|
|
360
540
|
// ⚠ TWO AXES OUT OF ONE SELECTION. The user only ever touches rules; the request carries
|
|
361
541
|
// both `doctrines` (the packs with at least one rule left) and `excludeRules` (the rules
|
|
362
|
-
// dropped from the packs that survive).
|
|
363
|
-
|
|
364
|
-
const applyPacks = () => {
|
|
542
|
+
// dropped from the packs that survive).
|
|
543
|
+
const applyPacks = async () => {
|
|
365
544
|
if (!canApply || !dirty) return
|
|
366
545
|
const keptPacks = packs.filter((p) => p.rules.some((r) => isOn(r.addr))).map((p) => p.pack)
|
|
367
546
|
const excludeRules = packs
|
|
368
547
|
.filter((p) => keptPacks.includes(p.pack))
|
|
369
548
|
.flatMap((p) => p.rules.filter((r) => !isOn(r.addr)).map((r) => r.addr))
|
|
370
549
|
const warn = losingConsent
|
|
371
|
-
? '\n\
|
|
550
|
+
? '\n\nprocess 5 carries commit/push consent. Dropping it also removes the git-gate ' +
|
|
372
551
|
'hook, so commits and pushes stop being confirmed at the tool boundary. ' +
|
|
373
|
-
'(rm -rf and force-push stay gated
|
|
552
|
+
'(rm -rf and force-push stay gated: those are Rule IV’s, not the pack’s.)'
|
|
374
553
|
: ''
|
|
375
554
|
const what = pickedRules.length
|
|
376
555
|
? `${pickedRules.length} of ${allRules.length} doctrine rules`
|
|
377
556
|
: 'no doctrine rules'
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
`
|
|
381
|
-
|
|
382
|
-
)
|
|
557
|
+
const ok = await confirm(
|
|
558
|
+
`Rebuild ${install.host} · ${install.scope} with ${what}? ` +
|
|
559
|
+
`It rebuilds in place, non-destructive.${warn}`,
|
|
560
|
+
{ title: 'Rebuild with these rules?', confirmLabel: 'Rebuild' },
|
|
383
561
|
)
|
|
384
|
-
|
|
562
|
+
if (ok) onAction('install', { ...install, doctrines: keptPacks, excludeRules })
|
|
385
563
|
}
|
|
386
564
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
counts[l.cat] = (counts[l.cat] || 0) + 1
|
|
390
|
-
})
|
|
391
|
-
// ⚠ ONLY NON-EMPTY FACETS RENDER. Two of the six classes have no invariant since the split, and
|
|
392
|
-
// a chip reading `Context 0` is a filter that can only ever produce the empty state.
|
|
393
|
-
const facets = LAW_CAT_ORDER.filter((k) => counts[k])
|
|
394
|
-
// Substring narrowing over address, name and principle — display-only, so the
|
|
395
|
-
// staged pack selection above keeps operating on the full rule set.
|
|
565
|
+
// Substring narrowing over address, name, Latin name and principle; display-only, so the
|
|
566
|
+
// staged selection above keeps operating on the full rule set.
|
|
396
567
|
const ql = q.trim().toLowerCase()
|
|
397
|
-
const
|
|
398
|
-
|
|
568
|
+
const match = (r) => !ql || `${r.addr} ${r.name} ${r.latin} ${r.ess}`.toLowerCase().includes(ql)
|
|
569
|
+
// From the Gate asks card: make the rule's row visible first (a tab or a filter can hide
|
|
570
|
+
// it), then open it; the effect above scrolls there and lights it.
|
|
571
|
+
const jumpToRule = (addr) => {
|
|
572
|
+
const row = byAddr[addr]
|
|
573
|
+
const doctrine = String(addr).includes('.')
|
|
574
|
+
if ((doctrine && tab === 'invariants') || (!doctrine && tab === 'doctrines')) setTab('all')
|
|
575
|
+
if (row && !match(row)) setQ('')
|
|
576
|
+
jumpTo.current = addr
|
|
577
|
+
setJump((n) => n + 1)
|
|
578
|
+
go(`#/item/law/${encodeURIComponent(addr)}`)
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
const activeInv = laws.filter((l) => !l.retired).length
|
|
582
|
+
const activeDoc = deployedRules.length
|
|
583
|
+
const stats = rulesData?.stats
|
|
584
|
+
const tiers = [
|
|
585
|
+
{
|
|
586
|
+
key: 'ethos',
|
|
587
|
+
name: 'Ethos',
|
|
588
|
+
n: ontology.length,
|
|
589
|
+
fill: 1,
|
|
590
|
+
sub: `${ontology.length} sections`,
|
|
591
|
+
},
|
|
592
|
+
{
|
|
593
|
+
key: 'invariants',
|
|
594
|
+
name: 'Invariants',
|
|
595
|
+
n: activeInv,
|
|
596
|
+
fill: laws.length ? activeInv / laws.length : 0,
|
|
597
|
+
sub:
|
|
598
|
+
laws.length > activeInv
|
|
599
|
+
? `${activeInv} active · ${laws.length - activeInv} retired`
|
|
600
|
+
: `${activeInv} active`,
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
key: 'doctrines',
|
|
604
|
+
name: 'Doctrines',
|
|
605
|
+
n: activeDoc,
|
|
606
|
+
fill: allRules.length ? activeDoc / allRules.length : 0,
|
|
607
|
+
sub: `${activeDoc} rules · ${packs.filter((p) => p.active).length} of ${packs.length} packs`,
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
key: 'yours',
|
|
611
|
+
name: 'Your rules',
|
|
612
|
+
n: stats?.rules ?? 0,
|
|
613
|
+
fill: stats ? stats.rules / stats.max_rules : 0,
|
|
614
|
+
sub: stats
|
|
615
|
+
? `${stats.rules} of ${stats.max_rules} · ${stats.tokens}/${stats.max_tokens} tok`
|
|
616
|
+
: 'user-rules.md',
|
|
617
|
+
href: '#/personal/rules',
|
|
618
|
+
},
|
|
619
|
+
]
|
|
620
|
+
const byAddr = Object.fromEntries(
|
|
621
|
+
[...laws, ...packs.flatMap((p) => p.rules)].map((r) => [r.addr, r]),
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
const TABS_ = [
|
|
625
|
+
['all', 'All', laws.length + allRules.length],
|
|
626
|
+
['invariants', 'Invariants', laws.length],
|
|
627
|
+
['doctrines', 'Doctrines', allRules.length],
|
|
628
|
+
]
|
|
629
|
+
const showInv = tab === 'all' || tab === 'invariants'
|
|
630
|
+
const showDoc = tab === 'all' || tab === 'doctrines'
|
|
631
|
+
const bandHead = (title, src) => (
|
|
632
|
+
<div className="band-head" role="presentation">
|
|
633
|
+
<h2 className="tier-h">{title}</h2>
|
|
634
|
+
<span className="mono dim">{src}</span>
|
|
635
|
+
</div>
|
|
636
|
+
)
|
|
637
|
+
const invRows = laws.filter(match)
|
|
638
|
+
const shownCount =
|
|
639
|
+
(showInv ? invRows.length : 0) +
|
|
640
|
+
(showDoc ? packs.reduce((n, p) => n + p.rules.filter(match).length, 0) : 0)
|
|
641
|
+
const maxPack = Math.max(1, ...packs.map((p) => p.rules.length))
|
|
399
642
|
|
|
400
643
|
return (
|
|
401
644
|
<>
|
|
402
|
-
<div className="head
|
|
645
|
+
<div className="page-head">
|
|
403
646
|
<div>
|
|
404
|
-
<div className="eyebrow">governance</div>
|
|
405
647
|
<h1 className="h">Constitution</h1>
|
|
406
648
|
<p className="sub">
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
install chose at build time. Open any entry to read its canonical text.
|
|
649
|
+
What the agent follows on every turn. A higher tier always wins. Open any rule to read
|
|
650
|
+
its canonical text.
|
|
410
651
|
</p>
|
|
411
652
|
</div>
|
|
412
653
|
</div>
|
|
654
|
+
<TierStrip tiers={tiers} current={tab} />
|
|
413
655
|
|
|
414
|
-
<div className="
|
|
415
|
-
<
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
656
|
+
<div className="split">
|
|
657
|
+
<section
|
|
658
|
+
className={`panel flush law-wrap${canApply && showDoc ? ' with-toggle' : ''}`}
|
|
659
|
+
aria-label="Rules"
|
|
660
|
+
>
|
|
661
|
+
{/* Ethos sits ABOVE the tabs and the filter, and neither touches it: its four
|
|
662
|
+
sections are the character the rules below serve, not rules to narrow down, so
|
|
663
|
+
the tabs and "All" count only what the filter can actually reach. */}
|
|
664
|
+
{ontology.length > 0 && (
|
|
665
|
+
<>
|
|
666
|
+
{bandHead('Ethos', 'ontology/universal.md')}
|
|
667
|
+
{ontology.map((s) => (
|
|
668
|
+
<LawRow
|
|
669
|
+
key={s.addr}
|
|
670
|
+
law={s}
|
|
671
|
+
isOpen={open === s.addr}
|
|
672
|
+
onToggle={() => toggle(s.addr)}
|
|
673
|
+
/>
|
|
674
|
+
))}
|
|
675
|
+
</>
|
|
676
|
+
)}
|
|
677
|
+
<div className="law-toolbar">
|
|
678
|
+
<Seg aria-label="Show">
|
|
679
|
+
{TABS_.map(([k, l, n]) => (
|
|
680
|
+
<button
|
|
681
|
+
type="button"
|
|
682
|
+
key={k}
|
|
683
|
+
className={tab === k ? 'on' : ''}
|
|
684
|
+
aria-pressed={tab === k}
|
|
685
|
+
onClick={() => setTab(k)}
|
|
686
|
+
>
|
|
687
|
+
{l} <span className="mono">{n}</span>
|
|
688
|
+
</button>
|
|
689
|
+
))}
|
|
690
|
+
</Seg>
|
|
691
|
+
<FilterInput
|
|
692
|
+
className="law-filter"
|
|
693
|
+
value={q}
|
|
694
|
+
onChange={setQ}
|
|
695
|
+
placeholder="Filter rules"
|
|
696
|
+
label="Filter rules"
|
|
697
|
+
/>
|
|
698
|
+
<span className="dim law-note">Retired rules keep their number.</span>
|
|
433
699
|
</div>
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
<div className="law-toolbar">
|
|
441
|
-
<div className="law-cats">
|
|
442
|
-
<button className={`law-cat ${sel === 'all' ? 'on' : ''}`} onClick={() => setSel('all')}>
|
|
443
|
-
<span>All</span>
|
|
444
|
-
<span className="cn">{laws.length}</span>
|
|
445
|
-
</button>
|
|
446
|
-
{facets.map((k) => (
|
|
447
|
-
<button
|
|
448
|
-
key={k}
|
|
449
|
-
className={`law-cat ${sel === k ? 'on' : ''}`}
|
|
450
|
-
style={{ '--cc': LAW_CATS[k].c }}
|
|
451
|
-
onClick={() => setSel(k)}
|
|
452
|
-
>
|
|
453
|
-
<span className="cdot" />
|
|
454
|
-
<span>{LAW_CATS[k].label}</span>
|
|
455
|
-
<span className="cn">{counts[k] || 0}</span>
|
|
456
|
-
</button>
|
|
457
|
-
))}
|
|
458
|
-
</div>
|
|
459
|
-
<FilterInput
|
|
460
|
-
className="lib-filter law-filter"
|
|
461
|
-
value={q}
|
|
462
|
-
onChange={setQ}
|
|
463
|
-
placeholder="Filter rules…"
|
|
464
|
-
label="Filter rules"
|
|
465
|
-
/>
|
|
466
|
-
<span className="law-readout">
|
|
467
|
-
{/* Derived, never transcribed: a hardcoded 6 outlived the corpus it counted once. */}
|
|
468
|
-
<b>{shown.length}</b> invariants · <b>{facets.length}</b> classes · source{' '}
|
|
469
|
-
<b>laws/universal.md</b>
|
|
470
|
-
</span>
|
|
471
|
-
</div>
|
|
472
|
-
<div className="card law-wrap mb-16">
|
|
473
|
-
<div className="law-rowhead">
|
|
474
|
-
<span>№</span>
|
|
475
|
-
<span>Rule</span>
|
|
476
|
-
<span>Principle</span>
|
|
477
|
-
<span>Class</span>
|
|
478
|
-
</div>
|
|
479
|
-
{shown.map((l) => (
|
|
480
|
-
<LawRow key={l.addr} law={l} isOpen={open === l.addr} onToggle={() => toggle(l.addr)} />
|
|
481
|
-
))}
|
|
482
|
-
{shown.length === 0 && (
|
|
483
|
-
<div className="empty" style={{ padding: 32 }}>
|
|
484
|
-
<div className="big">{ql ? 'No matching rules' : 'No rules in this class'}</div>
|
|
485
|
-
{ql ? <>Nothing matches “{q.trim()}”.</> : <>Try another class, or pick All.</>}
|
|
700
|
+
<div className="law-rowhead">
|
|
701
|
+
<span>No.</span>
|
|
702
|
+
<span>Rule</span>
|
|
703
|
+
<span>Latin name</span>
|
|
704
|
+
<span>Status</span>
|
|
705
|
+
<span>Enforced by</span>
|
|
486
706
|
</div>
|
|
487
|
-
|
|
488
|
-
|
|
707
|
+
{showInv && invRows.length > 0 && (
|
|
708
|
+
<>
|
|
709
|
+
{bandHead('Invariants', 'laws/universal.md')}
|
|
710
|
+
{invRows.map((l) => (
|
|
711
|
+
<LawRow
|
|
712
|
+
key={l.addr}
|
|
713
|
+
law={l}
|
|
714
|
+
isOpen={open === l.addr}
|
|
715
|
+
onToggle={() => toggle(l.addr)}
|
|
716
|
+
/>
|
|
717
|
+
))}
|
|
718
|
+
</>
|
|
719
|
+
)}
|
|
720
|
+
{showDoc &&
|
|
721
|
+
packs.map((p) => {
|
|
722
|
+
const rows = p.rules.filter(match)
|
|
723
|
+
if (!rows.length) return null
|
|
724
|
+
return (
|
|
725
|
+
<div className={`pack-wrap${packOn(p) ? '' : ' pack-off'}`} key={p.pack}>
|
|
726
|
+
<div className="band-head pack-head">
|
|
727
|
+
<h2 className="tier-h" style={{ '--cc': PACK_CATS[p.pack] }}>
|
|
728
|
+
{p.title}
|
|
729
|
+
</h2>
|
|
730
|
+
<span className="pack-desc dim">{p.desc}</span>
|
|
731
|
+
{/* Derived from the rules, never held separately: a pack IS however many
|
|
732
|
+
of its rules are on. */}
|
|
733
|
+
<span className="pack-state mono">{packState(p)}</span>
|
|
734
|
+
</div>
|
|
735
|
+
{!p.active && !canApply && (
|
|
736
|
+
// The fallback for a console with no install to rebuild: a reader still
|
|
737
|
+
// needs the exact selection, because `--doctrines` REPLACES the set.
|
|
738
|
+
// ⚠ `geneseed-build`, not `geneseed build`: the CLI's `build` verb
|
|
739
|
+
// forwards `--theme` and nothing else, so the shorter spelling errors.
|
|
740
|
+
<div className="pack-enable">
|
|
741
|
+
$ geneseed-build --doctrines{' '}
|
|
742
|
+
{packs
|
|
743
|
+
.filter((x) => x.active || x.pack === p.pack)
|
|
744
|
+
.map((x) => x.pack)
|
|
745
|
+
.join(',')}
|
|
746
|
+
{deployedExcluded.length > 0 &&
|
|
747
|
+
` --exclude-rules ${deployedExcluded.join(',')}`}
|
|
748
|
+
</div>
|
|
749
|
+
)}
|
|
750
|
+
{rows.map((r) => (
|
|
751
|
+
<LawRow
|
|
752
|
+
key={r.addr}
|
|
753
|
+
law={docRow(r)}
|
|
754
|
+
isOpen={open === r.addr}
|
|
755
|
+
onToggle={() => toggle(r.addr)}
|
|
756
|
+
toggleCol={
|
|
757
|
+
canApply ? (
|
|
758
|
+
<button
|
|
759
|
+
type="button"
|
|
760
|
+
className={`sw-toggle${isOn(r.addr) ? ' on' : ''}`}
|
|
761
|
+
role="switch"
|
|
762
|
+
aria-checked={isOn(r.addr)}
|
|
763
|
+
aria-label={`${r.name} rule`}
|
|
764
|
+
onClick={() => toggleRule(r.addr)}
|
|
765
|
+
/>
|
|
766
|
+
) : null
|
|
767
|
+
}
|
|
768
|
+
/>
|
|
769
|
+
))}
|
|
770
|
+
</div>
|
|
771
|
+
)
|
|
772
|
+
})}
|
|
773
|
+
{shownCount === 0 && (
|
|
774
|
+
<div className="empty" style={{ padding: 32 }}>
|
|
775
|
+
<div className="big">{ql ? 'No matching rules' : 'Nothing in this tier'}</div>
|
|
776
|
+
{ql ? <>Nothing matches “{q.trim()}”.</> : <>This install predates the tier.</>}
|
|
777
|
+
</div>
|
|
778
|
+
)}
|
|
779
|
+
</section>
|
|
489
780
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
</div>
|
|
497
|
-
{packs.map((p) => {
|
|
498
|
-
// The filter narrows the doctrine bands too; a pack with no matching
|
|
499
|
-
// rule drops out entirely rather than rendering an empty shell.
|
|
500
|
-
const packRules = p.rules.filter(ruleMatch)
|
|
501
|
-
if (ql && packRules.length === 0) return null
|
|
502
|
-
return (
|
|
503
|
-
<div
|
|
504
|
-
className={`card law-wrap mb-16 pack-wrap ${packOn(p) ? '' : 'pack-off'}`}
|
|
505
|
-
key={p.pack}
|
|
506
|
-
>
|
|
507
|
-
<div className="pack-head">
|
|
508
|
-
<span className="pack-name" style={{ '--cc': PACK_CATS[p.pack] }}>
|
|
509
|
-
<span className="cdot" />
|
|
510
|
-
{p.title}
|
|
781
|
+
<aside className="aside-stack">
|
|
782
|
+
<section className="panel" aria-labelledby="doc-packs">
|
|
783
|
+
<div className="panel-head">
|
|
784
|
+
<h2 id="doc-packs">Doctrine packs</h2>
|
|
785
|
+
<span className="mono dim">
|
|
786
|
+
{packs.filter(packOn).length}/{packs.length} on
|
|
511
787
|
</span>
|
|
512
|
-
<span className="pack-desc">{p.desc}</span>
|
|
513
|
-
{/* Derived from the rules, never held separately — a pack IS however many of its
|
|
514
|
-
rules are on, and a second source could disagree with the switches above it. */}
|
|
515
|
-
<span className="pack-state">{packState(p)}</span>
|
|
516
788
|
</div>
|
|
517
|
-
{
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
789
|
+
{packs.length ? (
|
|
790
|
+
<ul className="pack-list">
|
|
791
|
+
{packs.map((p) => {
|
|
792
|
+
const on = p.rules.filter((r) => isOn(r.addr)).length
|
|
793
|
+
return (
|
|
794
|
+
<li key={p.pack} className={packOn(p) ? '' : 'off'}>
|
|
795
|
+
<span className="pl-name" title={p.desc}>
|
|
796
|
+
{p.title}
|
|
797
|
+
</span>
|
|
798
|
+
<span className="hbar" aria-hidden="true">
|
|
799
|
+
<span style={{ width: `${(on / maxPack) * 100}%` }} />
|
|
800
|
+
</span>
|
|
801
|
+
<span className="mono pl-n">{on}</span>
|
|
802
|
+
{canApply ? (
|
|
803
|
+
<button
|
|
804
|
+
type="button"
|
|
805
|
+
className={`sw-toggle${packOn(p) ? ' on' : ''}`}
|
|
806
|
+
role="switch"
|
|
807
|
+
aria-checked={packOn(p)}
|
|
808
|
+
aria-label={`${p.title} pack`}
|
|
809
|
+
onClick={() => togglePack(p)}
|
|
810
|
+
/>
|
|
811
|
+
) : null}
|
|
812
|
+
</li>
|
|
813
|
+
)
|
|
814
|
+
})}
|
|
815
|
+
</ul>
|
|
816
|
+
) : (
|
|
817
|
+
<p className="sub">This install predates the three-tier constitution.</p>
|
|
530
818
|
)}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
⚠ Dropping <b>process 5</b> also removes the commit/push consent gate —{' '}
|
|
566
|
-
<code>git commit</code> and <code>git push</code> stop being confirmed at the tool
|
|
567
|
-
boundary. <code>rm -rf</code> and force-push stay gated.
|
|
568
|
-
</p>
|
|
569
|
-
)}
|
|
570
|
-
<div className="pack-apply">
|
|
571
|
-
<button className="btn soft" onClick={applyPacks} disabled={!dirty}>
|
|
572
|
-
Apply{dirty ? ` (${pickedRules.length}/${allRules.length} rules)` : ''}
|
|
573
|
-
</button>
|
|
574
|
-
<button
|
|
575
|
-
className="btn ghost"
|
|
576
|
-
onClick={() => setPicked(deployedKey ? deployedKey.split(',') : [])}
|
|
577
|
-
disabled={!dirty}
|
|
578
|
-
>
|
|
579
|
-
Revert
|
|
580
|
-
</button>
|
|
581
|
-
<span className="sub" role="status" aria-live="polite">
|
|
582
|
-
{dirty
|
|
583
|
-
? `Not applied yet — Apply rebuilds ${install.host} · ${install.scope} once, ` +
|
|
584
|
-
'with every change together.'
|
|
585
|
-
: 'Matches the deployed install.'}
|
|
586
|
-
</span>
|
|
587
|
-
</div>
|
|
588
|
-
</div>
|
|
589
|
-
)}
|
|
590
|
-
{packs.length === 0 && (
|
|
591
|
-
<div className="card law-wrap">
|
|
592
|
-
<div className="empty" style={{ padding: 32 }}>
|
|
593
|
-
<div className="big">No doctrine packs</div>
|
|
594
|
-
This install predates the three-tier constitution.
|
|
595
|
-
</div>
|
|
596
|
-
</div>
|
|
597
|
-
)}
|
|
819
|
+
{canApply && packs.length > 0 && (
|
|
820
|
+
<>
|
|
821
|
+
{losingConsent && (
|
|
822
|
+
<p className="pack-warn" role="status" aria-live="polite">
|
|
823
|
+
Dropping <b>process 5</b> also removes the commit/push consent gate:{' '}
|
|
824
|
+
<code>git commit</code> and <code>git push</code> stop being confirmed at the
|
|
825
|
+
tool boundary. <code>rm -rf</code> and force-push stay gated.
|
|
826
|
+
</p>
|
|
827
|
+
)}
|
|
828
|
+
<div className="pack-apply">
|
|
829
|
+
<button type="button" className="btn" onClick={applyPacks} disabled={!dirty}>
|
|
830
|
+
Apply{dirty ? ` (${pickedRules.length}/${allRules.length} rules)` : ''}
|
|
831
|
+
</button>
|
|
832
|
+
<button
|
|
833
|
+
type="button"
|
|
834
|
+
className="btn ghost"
|
|
835
|
+
onClick={() => setPicked(deployedKey ? deployedKey.split(',') : [])}
|
|
836
|
+
disabled={!dirty}
|
|
837
|
+
>
|
|
838
|
+
Revert
|
|
839
|
+
</button>
|
|
840
|
+
</div>
|
|
841
|
+
<p className="panel-note" role="status" aria-live="polite">
|
|
842
|
+
{dirty
|
|
843
|
+
? `Not applied yet. Apply rebuilds ${install.host} · ${install.scope} once, ` +
|
|
844
|
+
'with every change together.'
|
|
845
|
+
: 'Matches the deployed install. Changing a pack rebuilds it.'}
|
|
846
|
+
</p>
|
|
847
|
+
</>
|
|
848
|
+
)}
|
|
849
|
+
</section>
|
|
850
|
+
<GateAsks gates={setup?.gates} byAddr={byAddr} onJump={jumpToRule} />
|
|
851
|
+
</aside>
|
|
852
|
+
</div>
|
|
598
853
|
</>
|
|
599
854
|
)
|
|
600
855
|
}
|