geneseed 4.18.1 → 4.19.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.
Files changed (47) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/DESIGN.md +5 -5
  3. package/SHIPPED.md +1 -1
  4. package/docs/extending.md +22 -30
  5. package/docs/web-ui.md +1 -1
  6. package/harness.config.json +1 -1
  7. package/js/README.md +1 -1
  8. package/js/build/source.mjs +27 -0
  9. package/js/inspect/checks-authoring.mjs +1 -1
  10. package/js/inspect/checks-constitution.mjs +66 -135
  11. package/js/inspect/checks-counts.mjs +3 -6
  12. package/js/web/catalog.mjs +11 -4
  13. package/js/web/user-files.mjs +1 -1
  14. package/package.json +1 -2
  15. package/src/doctrines/comms.md +5 -0
  16. package/src/doctrines/craft.md +7 -0
  17. package/src/doctrines/ops.md +7 -0
  18. package/src/doctrines/process.md +7 -0
  19. package/src/doctrines/rigor.md +5 -0
  20. package/src/laws/universal.md +11 -1
  21. package/src/skills/rule.md +4 -2
  22. package/web/dist/assets/{Activity-CrBz-58G.js → Activity-2fa9G39h.js} +1 -1
  23. package/web/dist/assets/{ActivityDetail-ZgsHrwvj.js → ActivityDetail-DIT7ITJw.js} +1 -1
  24. package/web/dist/assets/Dashboard-Ddz0KQF3.js +3 -0
  25. package/web/dist/assets/{Docs-f2YJDQah.js → Docs-j5cPrsOs.js} +1 -1
  26. package/web/dist/assets/{GreenhouseView-De8W1BvM.js → GreenhouseView-BhI_tF4c.js} +1 -1
  27. package/web/dist/assets/{Installs-BFsJIIti.js → Installs-kkE0H4Q2.js} +3 -3
  28. package/web/dist/assets/JournalView-w5adl4Sm.js +1 -0
  29. package/web/dist/assets/{KitApply-BYyktcdw.js → KitApply-BAF0gtXb.js} +1 -1
  30. package/web/dist/assets/{Kits-Dfch112m.js → Kits-BteVDSKD.js} +1 -1
  31. package/web/dist/assets/Laws-DNTpf31b.js +3 -0
  32. package/web/dist/assets/{Library-CrZGLGWE.js → Library-CgX1dJTr.js} +1 -1
  33. package/web/dist/assets/{Loops-BsbkBFl-.js → Loops-MhJU2d8B.js} +1 -1
  34. package/web/dist/assets/{OperatorHudView-BB8BH5PL.js → OperatorHudView-BvLd1zig.js} +1 -1
  35. package/web/dist/assets/Personal-BdN0jlmh.js +1 -0
  36. package/web/dist/assets/{Tabs-b-L-ra4k.js → Tabs-BPs85vgk.js} +1 -1
  37. package/web/dist/assets/{drawable-CuQpH-f5.js → drawable-DjpisR6c.js} +1 -1
  38. package/web/dist/assets/index-B9-bvRjd.js +2 -0
  39. package/web/dist/assets/index-DwzZK5uG.css +1 -0
  40. package/web/dist/index.html +2 -2
  41. package/web/dist/assets/Dashboard-Ds7_blrK.js +0 -3
  42. package/web/dist/assets/JournalView-DZHR1l1P.js +0 -1
  43. package/web/dist/assets/Laws-BztN3C8x.js +0 -3
  44. package/web/dist/assets/Personal-LRrDvMje.js +0 -1
  45. package/web/dist/assets/index--3760QWr.js +0 -2
  46. package/web/dist/assets/index-FWb-1Qgz.css +0 -1
  47. package/web/src/pages/Laws.jsx +0 -683
package/CHANGELOG.md CHANGED
@@ -8,6 +8,22 @@ label. For the capability ↔ spec map, see [SHIPPED.md](SHIPPED.md).
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [4.19.0] — 2026-10-24
12
+
13
+ Minor: shared console building blocks, rule summaries kept next to their rules, and `global` as a documented rule scope.
14
+
15
+ ### Added
16
+ - **Shared console building blocks.** `PageHeader` (one primary action), `EmptyState`, `DataTable`, `Stat`, `Bar`/`StackBar`, `Ring` and `Popover`, on a 7-step type scale, an 8-step spacing scale and named z-index layers. Pages adopt them in the next release.
17
+ - **`global` is a documented rule scope.** The `rule` skill names `project`, `global` and `user`, and the console's scope select offers `global`. A scope it does not list is still kept as written.
18
+
19
+ ### Changed
20
+ - **Rule summaries live next to their rules.** The one-line summary the Constitution page prints for each law and doctrine rule is now a `<!-- note: summary: … -->` line under the rule's heading in `src/`, served by the catalog; the page's own copy of the 40 summaries is gone. The text moved word for word, and installs are byte-identical. `doctor` checks one summary per rule, none orphaned.
21
+ - **Destructive confirmations look destructive.** Forget, Retire, Restore, Stop server and discarding an unsaved edit use a danger button and put focus on Cancel. An error toast stays until you dismiss it.
22
+
23
+ ### Fixed
24
+ - The danger button's text now meets WCAG AA on dark backgrounds in every flavour.
25
+ - The Overview lenses' readiness ring follows the fleet verdict (capped at 65% when the fleet needs a look, 40% when it is in trouble), and the Constitution map and KPI strip use the voice's own tier names.
26
+
11
27
  ## [4.18.1] — 2026-10-24
12
28
 
13
29
  Patch: the web console's light mode meets WCAG AA, and console installs default to lean.
package/DESIGN.md CHANGED
@@ -150,9 +150,9 @@ vault or a specific tool's hooks.
150
150
  | body | `### {{LAW:<id>}} <Principle>` in `src/laws/universal.md`; citations resolve by id, so no position rewires one | `### {{DOCTRINE:<id>}} <Principle>` in `src/doctrines/<pack>.md`; the number is its position | a new `src/doctrines/<pack>.md` with its `**Name** — lead line` |
151
151
  | themed title | `LEX_<ID>` in all 15 theme files | `DOC_<ID>` in all 15 theme files | `PACK_<NAME>` in all 15, plus each rule's `DOC_*` |
152
152
  | class | `LAW_CLASS` in `js/inspect/inventory.mjs`, one of the six in `LAW_CLASSES` | **none** — a doctrine rule's class *is* its pack, and there is no second taxonomy over it | — |
153
- | console copy | a `LAW_META` row in `web/src/pages/Laws.jsx` (keyed by arabic number) | a `DOCTRINE_META` row in the same file (keyed `<pack>.<n>`) | one `DOCTRINE_META` row per rule |
153
+ | console copy | a `<!-- note: summary: … -->` line under the rule's heading | the same line under each rule's heading | one summary line per rule |
154
154
  | registration | the numeral is the registration | the number is the registration | the pack name in `PACK_ORDER`, `js/build/source.mjs` |
155
- | renumber risk | none for citations (by id); `LAW_META`/`LAW_CLASS` are number-keyed and doctor-pinned, so append by preference | none for citations; `DOCTRINE_META`, exclusions and hook ledger keys are `<pack>.<n>` — append by preference | none |
155
+ | renumber risk | none for citations (by id); `LAW_CLASS` is number-keyed and doctor-pinned, so append by preference | none for citations; exclusions and hook ledger keys are `<pack>.<n>` — append by preference | none |
156
156
  | counts | README badge + the `N universal laws` prose, `SHIPPED.md`'s triple, the web onboarding copy | **none** — the console spends the `N_PACKS` / `N_PACKS_ACTIVE` / `N_DOCTRINE_RULES` count tokens, all computed at request time | none, same reason |
157
157
  | changelog | `CHANGELOG.md` | `CHANGELOG.md` | `CHANGELOG.md` |
158
158
 
@@ -163,9 +163,9 @@ vault or a specific tool's hooks.
163
163
  synonym. `--sync-themes` writes the template's placeholder and only *prints* which keys
164
164
  to restyle; a shipped placeholder passes every gate.
165
165
 
166
- Almost all of it is gated, across six named checks in `js/inspect/checks-build.mjs` —
167
- `themeParityProblems` (key parity across the voices), `lawMetaProblems` and
168
- `doctrineMetaProblems` (the console's Principle column, in both directions),
166
+ Almost all of it is gated, across five named checks in `js/inspect/checks-build.mjs` —
167
+ `themeParityProblems` (key parity across the voices), `ruleSummaryProblems`
168
+ (one console summary per rule, none orphaned),
169
169
  `constitutionProblems` (rule ids declared, unique and never retired, `LEX_*` as an *equality*,
170
170
  the `DOC_*` vocabulary, the hook-gated addresses, and every `{{LAW:…}}`/`{{DOCTRINE:…}}`
171
171
  citation in `src/` — a citation by number is refused), and
package/SHIPPED.md CHANGED
@@ -20,7 +20,7 @@ historical references, not files distributed with the repo.
20
20
  > **On version numbers.** [`harness.config.json`](harness.config.json)'s `version` is a
21
21
  > human-readable label only. The canonical identity of an installed harness is the
22
22
  > **source fingerprint** stamped in `.geneseed-version` (see `geneseed version`), not the
23
- > string. Current label: **4.18.1**.
23
+ > string. Current label: **4.19.0**.
24
24
 
25
25
  ## Capabilities
26
26
 
package/docs/extending.md CHANGED
@@ -179,7 +179,7 @@ not.
179
179
 
180
180
  ### 2a — Adding a DOCTRINE RULE (the common case)
181
181
 
182
- Five files, plus the written-out rule totals in four test files. This is the slot most new
182
+ Four files, plus the written-out rule totals in four test files. This is the slot most new
183
183
  material belongs in, and it is cheap on purpose so that the invariant slot stays scarce.
184
184
 
185
185
  1. `src/doctrines/<pack>.md` — add `### {{DOCTRINE:<new-id>}} <Principle Name>` plus the body,
@@ -188,8 +188,11 @@ material belongs in, and it is cheap on purpose so that the invariant slot stays
188
188
  (the style rule is in `src/doctrines/README.md`, and `leanBlockProblems` refuses a rule with no
189
189
  block or an empty half). The id is kebab-case, unique across laws **and** doctrines, and never
190
190
  one listed in `RETIRED_RULE_IDS` (`js/build/source.mjs`). Where it goes in the file decides its
191
- number; appending is still the kind move, because `excludeRules`, `DOCTRINE_META` and the
192
- hook-gated addresses are keyed `<pack>.<n>` (see §2e).
191
+ number; appending is still the kind move, because `excludeRules` and the hook-gated addresses
192
+ are keyed `<pack>.<n>` (see §2e). On the line directly under the heading, above `LEAN:begin`,
193
+ add `<!-- note: summary: <one line> -->` — the one-line Principle the console prints under the
194
+ rule's name. **That copy exists nowhere else**, and `ruleSummaryProblems` refuses a rule with
195
+ none, two, an empty one or one off its line. Being an author note, it never reaches an emit.
193
196
  2. `themes/_TEMPLATE.json` — add `DOC_<ID>` (`consent-before-push` → `DOC_CONSENT_BEFORE_PUSH`).
194
197
  ⚠ **This file *is* gated for this key family.**
195
198
  `themeFiles()` filters `_`-prefixed names and theme parity never sees the template, but
@@ -206,15 +209,7 @@ material belongs in, and it is cheap on purpose so that the invariant slot stays
206
209
  is one of the two. See §7.2 before you copy a command from anywhere else. Then restyle all
207
210
  fourteen by hand: `--sync-themes` writes the template's placeholder and only *prints*
208
211
  `RESTYLE these`; a shipped placeholder passes every gate.
209
- 4. `web/src/pages/Laws.jsx` — a `DOCTRINE_META` row keyed `'<pack>.<n>'`, whose third field is the
210
- rule's id — `doctrineMetaProblems` holds that pin to the canon, so a row left behind by a
211
- renumber is a failure rather than a description of the neighbour. Its first field is the
212
- **pack**, not one of `LAW_CLASSES`' six; `doctrineMetaProblems` requires an *equality* between
213
- that field and the key's own pack, which catches the copy error a six-way vocabulary cannot — a
214
- row pasted from the pack above it, keeping the wrong pack. Its second field is the one-line
215
- Principle, and **that copy exists nowhere else**; a rule absent here renders with a blank
216
- description.
217
- 5. `CHANGELOG.md`, then rebuild and commit `web/dist` (step 4 touched `web/src`).
212
+ 4. `CHANGELOG.md`.
218
213
 
219
214
  **What a doctrine rule does *not* cost**, and each of these is a deliberate asymmetry:
220
215
 
@@ -243,7 +238,7 @@ a defect.
243
238
 
244
239
  ### 2b — Amending an INVARIANT
245
240
 
246
- Steps 1 and 9 of §2c, plus `LAW_META` if the Principle line moved. Two cautions:
241
+ Steps 1 and 8 of §2c, plus the rule's `<!-- note: summary: … -->` line if the Principle moved. Two cautions:
247
242
 
248
243
  - `--footprint` defaults to **lean**, and a law's lean text is the authored `LEAN:else` half of
249
244
  its block in `src/laws/universal.md` (`resolveLean` in `js/build/render.mjs`). Amend **both**
@@ -253,12 +248,14 @@ Steps 1 and 9 of §2c, plus `LAW_META` if the Principle line moved. Two cautions
253
248
 
254
249
  ### 2c — Adding an INVARIANT
255
250
 
256
- Nine steps, and only the first is the rule itself. This is the expensive one, and it should be:
251
+ Eight steps, and only the first is the rule itself. This is the expensive one, and it should be:
257
252
  the bar is in `DESIGN.md` Decision 7, and most candidates belong in a doctrine pack instead.
258
253
 
259
254
  1. `src/laws/universal.md` — add `### {{LAW:<new-id>}} <Principle Name>` plus the body. Citations
260
255
  are by id and resolve against the canon, so position is safe to choose — but append unless the
261
- order is the point: `LAW_CLASS` and `LAW_META` are keyed by the rendered number (see §2e).
256
+ order is the point: `LAW_CLASS` is keyed by the rendered number (see §2e). On the line directly
257
+ under the heading, add the rule's `<!-- note: summary: <one line> -->`, as in §2a step 1 — the
258
+ console's one-line Principle, gated by `ruleSummaryProblems`.
262
259
  2. `themes/_TEMPLATE.json` — add `LEX_<ID>` in template order. Gated, as in §2a step 2:
263
260
  `constitutionProblems` holds the `LEX_*` keys as an **equality** with the declared law ids across
264
261
  all fifteen files (the fourteen voices and the template), so both a missing key *and* a leftover
@@ -266,16 +263,12 @@ the bar is in `DESIGN.md` Decision 7, and most candidates belong in a doctrine p
266
263
  `LEX_XXIV` and `LEX_XXXVI` behind in every file and parity was silent.
267
264
  3. `--sync-themes`, then restyle all fourteen by hand (§2a step 3).
268
265
  4. `LAW_CLASS` in `js/inspect/inventory.mjs` — one of the six in `LAW_CLASSES` beside it.
269
- 5. `LAW_META` in `web/src/pages/Laws.jsx` — same class, the one-line Principle, and the rule's id
270
- as the pin. Note the map is keyed by **arabic** number where the heading renders a Roman numeral. **This copy
271
- exists nowhere else**; a rule absent here renders with a blank description. It is why that one
272
- React file ships alone out of `web/src` in `package.json` — `doctor` regex-parses its literal.
273
- 6. `README.md` — badge `badge/laws-N` and the `N universal laws` sentence.
274
- 7. `SHIPPED.md` — the `N laws, N agents, N skills` triple.
275
- 8. `tests/unit/harness.test.mjs` — the negative fixture for the `LAW_CLASS` gate appends a law and
276
- expects the next numeral (`X` while there are nine), and `ROMANS` there and in `emit_smoke`
277
- lists the numerals written out. Bump both.
278
- 9. `CHANGELOG.md`, then rebuild and commit `web/dist` (step 5 touched `web/src`).
266
+ 5. `README.md` — badge `badge/laws-N` and the `N universal laws` sentence.
267
+ 6. `SHIPPED.md` — the `N laws, N agents, N skills` triple.
268
+ 7. `tests/unit/harness.test.mjs` — the negative fixture for the `LAW_CLASS` gate appends a law and
269
+ expects the next numeral (`X` while there are nine), and `ROMANS` in `emit_smoke` lists the
270
+ numerals written out. Bump both.
271
+ 8. `CHANGELOG.md`.
279
272
 
280
273
  ⚠ While restyling, `{{LAW}}` must stay a **single word** in every theme (and so must
281
274
  `{{DOCTRINE}}`). Both heading parsers match the tier noun with `\S+`, so a two-word value does not
@@ -341,8 +334,7 @@ render — no citation moves, because none names a number. Then:
341
334
  carries the text.
342
335
  3. Its `LEX_<ID>`/`DOC_<ID>` key in all fifteen theme files (the equality arm names each).
343
336
  4. The number-keyed tables, which the doctor checks against the canon: `LAW_CLASS`
344
- (`js/inspect/inventory.mjs`), `LAW_META`/`DOCTRINE_META` (their id pins fail on every shifted
345
- row), and — only if a hook-gated rule moved — `HOOK_PINNED` in `checks-constitution.mjs` names the
337
+ (`js/inspect/inventory.mjs`), and — only if a hook-gated rule moved — `HOOK_PINNED` in `checks-constitution.mjs` names the
346
338
  ledger keys, `CONSENT_RULE` and `HOOK_GATED` to move with it. `--exclude-rules` addresses are
347
339
  `<pack>.<n>` too, and an install that excluded a shifted doctrine rule excludes its neighbour
348
340
  after a re-emit: prefer appending in a pack, and say so in the CHANGELOG when you cannot.
@@ -422,8 +414,8 @@ Two traps worth knowing before you write the spec:
422
414
  re-opens bash on all three hosts (OpenCode gates it to `ask`), and `<!-- webfetch: allow -->`
423
415
  re-opens web fetch — the researcher is the one spec that carries it.
424
416
  - **The console has a fourth, ungated copy of the skill taxonomy.** `SKILL_CATS` in
425
- `web/src/lib/lawCats.js` mirrors `SKILL_CLASS` with nothing comparing them (contrast
426
- `LAW_META`, which *is* cross-checked). Inventing a new *category* means editing that file —
417
+ `web/src/lib/lawCats.js` mirrors `SKILL_CLASS` with nothing comparing them (contrast the
418
+ rules' classes, which the server sends from `LAW_CLASS`). Inventing a new *category* means editing that file —
427
419
  which drags in the `web/dist` rebuild — and an unknown category lands under Personal in the
428
420
  Library (`splitSkills` in `web/src/pages/Library.jsx`) with no warning.
429
421
 
@@ -769,7 +761,7 @@ only, at best, on its presence.
769
761
  | What | Where | Gated on |
770
762
  |---|---|---|
771
763
  | `§N` cross-references into `AGENT.md`'s anatomy | `src/skills/*`, `src/agents/*`, `adapters/**` | **range only.** `constitutionProblems` refuses a `§N` the template declares no section for — which catches a pointer past the end, the shape *removing* a section leaves. It cannot catch one that still resolves and now means something else, which is the shape *inserting* a section leaves, and is what actually happened when `## 2. Doctrines` pushed every later section down one. **Renumbering the anatomy means `grep -rn '§' src/ adapters/` and reading every hit.** Four satellites and a live OpenCode deny message shipped stale because the sweep stopped at `AGENT.md.tmpl` |
772
- | Per-law Principle lines | `web/src/pages/Laws.jsx` | presence and class only, never accuracy |
764
+ | Per-rule Principle lines | the `<!-- note: summary: … -->` line under each rule heading in `src/laws/universal.md` and `src/doctrines/*.md` | presence and placement only (`ruleSummaryProblems`), never accuracy |
773
765
  | 588 themed constitution titles (42 keys × 14 voices) — `LEX_<ID>` (one per law), `DOC_<ID>` (one per doctrine rule), `PACK_<NAME>` | `themes/*.json` | key presence only — a shipped placeholder is green. `LEX_*` and `DOC_*` are held across the template too, and in both directions; `PACK_*` only across the voices |
774
766
  | The README keyword enumerations | `README.md`, `docs/concepts/rules.md` | nothing |
775
767
  | The per-host map of what lands on a machine | `docs/understand/machine-opencode.md`, `docs/understand/machine-claude.md` | table shape only, never accuracy |
package/docs/web-ui.md CHANGED
@@ -101,7 +101,7 @@ streams the output of background jobs and keeps their history across reloads.
101
101
 
102
102
  - **Rules** (`#/rules`) — your own standing rules, read from and written to the
103
103
  `user-rules.md` beside the deployed AGENT.md (the seed-once file no update ever
104
- touches). List with scope (user/project, or any other scope written by hand, which a
104
+ touches). List with scope (project/user/global, or any other scope written by hand, which a
105
105
  save keeps as written) and trial chips, add/edit/retire forms (a body line starting
106
106
  `## ` is refused: the file would read it as a new rule heading), a
107
107
  **Graduate** action that adopts a trial rule for good, and a budget meter that turns
@@ -2,5 +2,5 @@
2
2
  "_comment": "version is a human-readable release label only; the canonical identity of an installed harness is the source fingerprint stamped in .geneseed-version (see `harness version`), not this string.",
3
3
  "theme": "neutral",
4
4
  "product": "Geneseed",
5
- "version": "4.18.1"
5
+ "version": "4.19.0"
6
6
  }
package/js/README.md CHANGED
@@ -125,7 +125,7 @@ Everything that inspects an already-built or already-deployed harness and report
125
125
  | `checks-repo.mjs` | Gates over the repo's own records: `registry.json`, committed secrets, the hook shim, vendored pins and folder skills, skill aliases and side files, this module map, the host tables, the scorecard ratchet |
126
126
  | `checks-authoring.mjs` | `authoringProblems`, the aggregate self-consistency gate: spec purpose lines, the learn-prompt literal, `node --check` over the plugins (the doctor's one default spawn), then the groups below |
127
127
  | `checks-probe.mjs` | `doctor --probe` (opt-in): runs each live install's own gate command through the host's shell on a must-refuse and a must-pass payload — the doctor's second spawn; the OpenCode guard plugin is imported and called in-process instead, through both plugin APIs |
128
- | `checks-constitution.mjs` | The constitution: rule declarations and citations, LEAN blocks, `HOOK_PINNED`, theme vocabulary, the console's `LAW_META`/`DOCTRINE_META` |
128
+ | `checks-constitution.mjs` | The constitution: rule declarations and citations, LEAN blocks, `HOOK_PINNED`, theme vocabulary, the per-rule summaries (`ruleSummaryProblems`) |
129
129
  | `checks-counts.mjs` | The hand-written counts: AGENT.md tables, README badges and prose, `SHIPPED.md`, held against `src/` |
130
130
  | `scan.mjs` | The shared walk/sort/strip/test primitives every `checks-*` uses. Owns no judgement itself |
131
131
  | `validate.mjs` | The `validate` verb (the generator's old `--validate-only`): renders a sandbox, scans it for tokens and dead links, own exit contract |
@@ -145,6 +145,33 @@ export function ruleCanon(src = SRC) {
145
145
  return canon;
146
146
  }
147
147
 
148
+ /**
149
+ * RULE SUMMARIES — the one-line Principle the web console prints under each rule's name. It lives
150
+ * on the line directly under the rule's heading as `<!-- note: summary: <text> -->`, so the copy
151
+ * sits beside the rule it describes and moves with it; being an author note, `stripAuthorNotes`
152
+ * removes the whole line before any render, and no emit carries it. Read from the RAW text, apart
153
+ * from `ruleCanon`, so the render path pays nothing for it. `ruleSummaryProblems`
154
+ * (`js/inspect/checks-constitution.mjs`) is the gate: one summary per rule, in that place.
155
+ */
156
+ export const SUMMARY_LINE_RE = /^<!--[ \t]*note:[ \t]*summary:[ \t]*(.*?)[ \t]*-->[ \t]*$/;
157
+ /** Anything that LOOKS like a summary — the gate's net, wider than `SUMMARY_LINE_RE` on purpose. */
158
+ export const SUMMARY_MARK_RE = /<!--[ \t]*note:[ \t]*summary:/;
159
+ export function ruleSummaries(src = SRC) {
160
+ const out = new Map();
161
+ for (const [kind, , file] of canonFiles(src)) {
162
+ let text;
163
+ try { text = readRawText(file); } catch { continue; }
164
+ for (const m of text.matchAll(DECL_RE)) {
165
+ if (m[1] !== kind) continue;
166
+ const start = m.index + m[0].length + 1;
167
+ const end = text.indexOf('\n', start);
168
+ const s = SUMMARY_LINE_RE.exec(text.slice(start, end === -1 ? undefined : end));
169
+ if (s && !out.has(m[2])) out.set(m[2], s[1]);
170
+ }
171
+ }
172
+ return out;
173
+ }
174
+
148
175
  /**
149
176
  * AUTHOR NOTES — `<!-- note: ... -->`, single or multi-line, is maintainer-only prose and must
150
177
  * never reach an agent (user decision 2026-10-11). Stripped here because `resolveRuleIds` is the
@@ -113,7 +113,7 @@ export function authorNoteProblems(src = SRC) {
113
113
  // Moved to the two sibling modules; re-exported so callers that import the authoring group as
114
114
  // one module keep working.
115
115
  export {
116
- HOOK_PINNED, constitutionProblems, doctrineMetaProblems, lawMetaProblems, leanBlockProblems,
116
+ HOOK_PINNED, constitutionProblems, leanBlockProblems, ruleSummaryProblems,
117
117
  } from './checks-constitution.mjs';
118
118
  export { countTableProblems, kitProblems, proseMirrorProblems } from './checks-counts.mjs';
119
119
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The CONSTITUTION gates — whether the laws and doctrine packs agree with themselves and with
3
3
  * everything keyed off them: rule declarations and citations, the LEAN blocks, the hook-pinned
4
- * addresses, the theme vocabulary, and the web console's per-rule Principle copy.
4
+ * addresses, the theme vocabulary, and the per-rule summary the web console prints.
5
5
  *
6
6
  * Split out of `checks-authoring.mjs`, which still aggregates these (and re-exports them for
7
7
  * its existing importers). Same contract as every check module here: an array of problem
@@ -9,7 +9,8 @@
9
9
  */
10
10
  import path from 'node:path';
11
11
  import {
12
- CONFIG, PACK_ORDER, RETIRED_RULE_IDS, ROOT, RULE_ID_RE, SRC, THEMES, canonFiles, ruleCanon, titleKey,
12
+ CONFIG, PACK_ORDER, RETIRED_RULE_IDS, RULE_ID_RE, SRC, SUMMARY_LINE_RE, SUMMARY_MARK_RE, THEMES, canonFiles, ruleCanon,
13
+ titleKey, toRoman,
13
14
  } from '../build/source.mjs';
14
15
  import { LEAN_BLOCK_RE, splitAtLawHeadings } from '../build/render.mjs';
15
16
  import { themeFiles } from '../hosts/installs.mjs';
@@ -18,138 +19,68 @@ import { parseJson, formatRepr } from '../lib/json.mjs';
18
19
  import { NOTE, isFile, rglob } from './scan.mjs';
19
20
 
20
21
  /**
21
- * The body of `const <name> = { … }` in the web Laws page — the one file out of `web/src` the
22
- * doctor reads. Answers `[body, null]`, or `[null, problem]` when the page is unreadable or the
23
- * literal is gone (`missing` is that problem's text).
24
- */
25
- function lawsPageLiteral(name, missing) {
26
- let text;
27
- try { text = readText(path.join(ROOT, 'web', 'src', 'pages', 'Laws.jsx')); } catch (e) {
28
- return [null, `[authoring] web/src/pages/Laws.jsx unreadable: ${e.message}`];
29
- }
30
- const block = new RegExp(`^const ${name} = \\{$([\\s\\S]*?)^\\}$`, 'm').exec(text);
31
- return block ? [block[1], null] : [null, missing];
32
- }
33
-
34
- /**
35
- * `_harness_build._LAW_META_ROW` — `<arabic>: ['<class>', '<principle>', '<id>'],` in the web page.
22
+ * Every rule carries exactly one one-line summary, on the line directly under its heading —
23
+ * `<!-- note: summary: <text> -->` — the Principle the web console prints under the rule's name.
36
24
  *
37
- * The third field PINS the row to a rule id. The key is the number the catalogue renders, and a
38
- * number is only a position: remove a law and every later row describes its predecessor's
39
- * neighbour, with every class still plausible. The pin is what makes that a doctor failure.
25
+ * The copy exists nowhere else, and a rule without it renders with a blank description while
26
+ * every count upstream reads correct: two laws shipped exactly that way when the copy lived in a
27
+ * table in `web/src/pages/Laws.jsx`. Beside the heading, a removed or reordered rule takes its
28
+ * summary with it, so no positional key or id pin is left to drift.
40
29
  *
41
- * Either quote style, because a principle carrying an apostrophe is double-quoted. Every
42
- * `\s*` spans newlines and the trailing comma before `]` is optional, so a row a prettier has
43
- * reflowed across several lines still matches — a gate that only understood the one-line form
44
- * silently lost it and left the law's Principle unguarded.
30
+ * PLACEMENT IS PART OF THE CONTRACT because `ruleSummaries` (`js/build/source.mjs`) reads only
31
+ * the line under the heading: a summary one line lower is stripped from every render all the
32
+ * same, and the console would show nothing. A summary above the first rule, or under any heading
33
+ * that declares no rule, describes no rule at all.
45
34
  *
46
- * `(?m)` is the `m` flag; JS has no inline form. The `.` inside the captures must NOT match a
47
- * newline (Python has no `re.S` here), which is JS's default.
35
+ * ONE DEFINITION OF A SUMMARY LINE: `SUMMARY_LINE_RE`, the reader's own, applied to the line as
36
+ * written. A line `SUMMARY_MARK_RE` catches but that regex refuses — indented, or wrapped over two
37
+ * lines — is malformed here, because the reader would hand the console nothing for it.
48
38
  */
49
- const LAW_META_ROW =
50
- /^[^\S\n]*(\d+):\s*\[\s*(['"])(.*?)\2\s*,\s*(['"])(.*?)\4\s*(?:,\s*(['"])(.*?)\6\s*)?,?\s*\]\s*,?[^\S\n]*$/gm;
51
-
52
- /**
53
- * `_harness_build._law_meta_problems` — the web Laws ledger's per-rule copy stays complete.
54
- *
55
- * `LAW_META` holds each law's one-line Principle, copy that exists nowhere else in the tree,
56
- * and a law with no row falls back to `['craft', '']`: the rule renders with a blank
57
- * description and the wrong class chip, while everything upstream is fine. That is exactly
58
- * how two laws shipped description-less.
59
- */
60
- export function lawMetaProblems(lawRules, lawClass, lawClasses) {
61
- const [block, refusal] = lawsPageLiteral('LAW_META', '[authoring] LAW_META literal not found '
62
- + "in web/src/pages/Laws.jsx — the web Laws ledger's Principle column would have no gate");
63
- if (refusal) return [refusal];
64
- const meta = new Map();
65
- for (const m of block.matchAll(LAW_META_ROW)) {
66
- meta.set(Number(m[1]), [m[3], m[5], m[7]]);
67
- }
39
+ export function ruleSummaryProblems(src = SRC) {
68
40
  const problems = [];
69
- const seen = new Set();
70
- for (const { n, label: roman, id } of lawRules) {
71
- seen.add(n);
72
- if (!meta.has(n)) {
73
- problems.push(`[authoring] laws/universal.md rule ${roman} (${n}) has no row in `
74
- + 'LAW_META (web/src/pages/Laws.jsx) — the web Laws ledger would '
75
- + 'render it with a blank Principle');
76
- continue;
77
- }
78
- const [klass, principle, pin] = meta.get(n);
79
- if (pin !== id) {
80
- problems.push(`[authoring] LAW_META[${n}] is pinned to '${pin ?? '(no id)'}' but rule ${roman} is `
81
- + `'${id}' — the rows no longer follow the canon's order, so this principle `
82
- + 'describes a different law');
83
- }
84
- if (!principle.trim()) {
85
- problems.push(`[authoring] LAW_META[${n}] has an empty principle line — rule `
86
- + `${roman} would render with a blank description`);
87
- }
88
- if (!lawClasses.includes(klass)) {
89
- problems.push(`[authoring] LAW_META[${n}] class '${klass}' is not a known class `
90
- + `${formatRepr(lawClasses)}`);
91
- } else if (lawClass[roman] && klass !== lawClass[roman]) {
92
- problems.push(`[authoring] LAW_META[${n}] classes rule ${roman} as '${klass}' but `
93
- + `LAW_CLASS says '${lawClass[roman]}'`);
94
- }
95
- }
96
- for (const n of [...meta.keys()].filter((k) => !seen.has(k)).sort((a, b) => a - b)) {
97
- problems.push(`[authoring] LAW_META lists rule ${n} but laws/universal.md has no such rule`);
98
- }
99
- return problems;
100
- }
101
-
102
- /** `LAW_META_ROW`'s twin for a STRING key — `'<pack>.<n>': ['<pack>', '<principle>', '<id>'],`. */
103
- const DOCTRINE_META_ROW =
104
- /^[^\S\n]*(['"])([a-z]+\.\d+)\1:\s*\[\s*(['"])(.*?)\3\s*,\s*(['"])(.*?)\5\s*(?:,\s*(['"])(.*?)\7\s*)?,?\s*\]\s*,?[^\S\n]*$/gm;
105
-
106
- /**
107
- * `lawMetaProblems`' doctrine half — the console's per-rule Principle column stays complete.
108
- *
109
- * SAME DEFECT, ONE TIER OVER. `DOCTRINE_META` holds copy that exists nowhere else in the tree,
110
- * and a rule with no row falls back to an empty principle: it renders with a blank description
111
- * while every count upstream reads correct. Two invariants shipped exactly that way before
112
- * `lawMetaProblems` existed, and 23 new rules is a much larger surface to lose one in.
113
- *
114
- * ⚠ THE CLASS ARM IS DIFFERENT FROM THE INVARIANTS'. An invariant's class is checked against
115
- * `LAW_CLASSES`, a vocabulary of six; a doctrine rule has no second taxonomy — its class IS its
116
- * pack — so the check is an EQUALITY against the pack in its own key. That catches the copy
117
- * error a six-way vocabulary cannot: a row pasted from the pack above it, keeping the class of
118
- * the pack it came from, which would colour the row and label its chip with the wrong pack.
119
- */
120
- export function doctrineMetaProblems(addrs, ids = null) {
121
- const [block, refusal] = lawsPageLiteral('DOCTRINE_META', '[authoring] DOCTRINE_META literal not '
122
- + "found in web/src/pages/Laws.jsx — the console's doctrine rows would have no gate on their "
123
- + 'Principle column');
124
- if (refusal) return [refusal];
125
- const meta = new Map();
126
- for (const m of block.matchAll(DOCTRINE_META_ROW)) meta.set(m[2], [m[4], m[6], m[8]]);
127
- const problems = [];
128
- const seen = new Set();
129
- for (const [i, addr] of addrs.entries()) {
130
- seen.add(addr);
131
- if (!meta.has(addr)) {
132
- problems.push(`[authoring] doctrine rule ${addr} has no row in DOCTRINE_META `
133
- + '(web/src/pages/Laws.jsx) — the console would render it with a blank Principle');
134
- continue;
135
- }
136
- const [klass, principle, pin] = meta.get(addr);
137
- if (ids && pin !== ids[i]) {
138
- problems.push(`[authoring] DOCTRINE_META['${addr}'] is pinned to '${pin ?? '(no id)'}' but `
139
- + `that address is '${ids[i]}' now — the row describes a different rule`);
140
- }
141
- if (!principle.trim()) {
142
- problems.push(`[authoring] DOCTRINE_META['${addr}'] has an empty principle line — that `
143
- + 'rule would render with a blank description');
144
- }
145
- const pack = addr.split('.')[0];
146
- if (klass !== pack) {
147
- problems.push(`[authoring] DOCTRINE_META['${addr}'] carries pack '${klass}' — a doctrine `
148
- + `rule's class IS its pack, so this must read '${pack}'`);
149
- }
150
- }
151
- for (const addr of [...meta.keys()].filter((k) => !seen.has(k)).sort()) {
152
- problems.push(`[authoring] DOCTRINE_META lists ${addr} but no pack file defines it`);
41
+ for (const [kind, pack, file] of canonFiles(src)) {
42
+ const rel = path.relative(src, file).split(path.sep).join('/');
43
+ let lines;
44
+ try { lines = readText(file).replace(/\r\n?/g, '\n').split('\n'); } catch { continue; } // constitutionProblems names it
45
+ const rules = [];
46
+ let current = null;
47
+ lines.forEach((line, i) => {
48
+ if (/^#{1,6} /.test(line)) {
49
+ const m = line.startsWith('### ') ? DECL_LINE_RE.exec(line) : null;
50
+ const id = m && m[1] === kind ? m[2] : null;
51
+ current = id ? { id, at: i, found: [] } : null;
52
+ if (current) rules.push(current);
53
+ return;
54
+ }
55
+ if (!SUMMARY_MARK_RE.test(line)) return;
56
+ if (current) current.found.push([i, SUMMARY_LINE_RE.exec(line)?.[1] ?? null]);
57
+ else {
58
+ problems.push(`[authoring] ${rel}: a summary line sits outside any rule declaration — it `
59
+ + 'describes no rule');
60
+ }
61
+ });
62
+ rules.forEach(({ id, at, found }, k) => {
63
+ const where = `[authoring] ${rel}: rule ${kind === 'LAW' ? toRoman(k + 1) : `${pack} ${k + 1}`} '${id}'`;
64
+ if (!found.length) {
65
+ problems.push(`${where} has no summary — the console would render it with a blank `
66
+ + "Principle; add '<!-- note: summary: <one line> -->' on the line directly under its heading");
67
+ return;
68
+ }
69
+ if (found.length > 1) {
70
+ problems.push(`${where} has ${found.length} summary lines — the console shows one; keep `
71
+ + 'exactly one');
72
+ }
73
+ if (found[0][0] !== at + 1) {
74
+ problems.push(`${where} summary is not directly under its heading — ruleSummaries reads `
75
+ + 'only that line, so the console would show none');
76
+ }
77
+ if (found[0][1] === null) {
78
+ problems.push(`${where} has a malformed summary line — it must be one unindented line, `
79
+ + "'<!-- note: summary: <text> -->', the only shape ruleSummaries reads");
80
+ } else if (!found[0][1].trim()) {
81
+ problems.push(`${where} has an empty summary — the console would render a blank Principle`);
82
+ }
83
+ });
153
84
  }
154
85
  return problems;
155
86
  }
@@ -242,8 +173,8 @@ export function leanBlockProblems() {
242
173
  }
243
174
 
244
175
  /**
245
- * The three-tier constitution's own authoring gates — the doctrine twin of the `LAW_CLASS` /
246
- * `LAW_META` family, plus the two purity rules the tiering rests on.
176
+ * The three-tier constitution's own authoring gates — the doctrine twin of the `LAW_CLASS`
177
+ * family, the per-rule summaries, plus the two purity rules the tiering rests on.
247
178
  *
248
179
  * IT LIVES BESIDE `countTableProblems` RATHER THAN INSIDE IT, and is called from
249
180
  * `authoringProblems` beside `registry`/`secret`/`vendorPin`. `countTableProblems` opens by
@@ -328,10 +259,10 @@ export function constitutionProblems() {
328
259
  }
329
260
  }
330
261
 
331
- // The console's Principle column, keyed off the rules just parsed — so the gate is driven by
332
- // the SOURCE and a rule added to a pack file is caught the same day, not when someone
333
- // notices a blank description in the browser.
334
- problems.push(...doctrineMetaProblems([...rules.keys()], [...rules.values()].map((r) => r.id)));
262
+ // The console's Principle column, read off the same files — so a rule added to the canon
263
+ // without its summary is caught the same day, not when someone notices a blank description
264
+ // in the browser.
265
+ problems.push(...ruleSummaryProblems());
335
266
 
336
267
  // ---- every citation in src/ is by id, and every id resolves.
337
268
  //
@@ -14,7 +14,6 @@ import { effectiveTheme, renderFile } from '../build/render.mjs';
14
14
  import { themeFiles } from '../hosts/installs.mjs';
15
15
  import { readText } from '../lib/fs.mjs';
16
16
  import { formatRepr } from '../lib/json.mjs';
17
- import { lawMetaProblems } from './checks-constitution.mjs';
18
17
  import { LAW_CLASS, LAW_CLASSES, SKILL_CLASS } from './inventory.mjs';
19
18
  import { globSorted, has, isDir, isFile, srcStems } from './scan.mjs';
20
19
  import { existsSync, readdirSync } from 'node:fs';
@@ -183,11 +182,10 @@ export function countTableProblems() {
183
182
  problems.push(`[authoring] SKILL_CLASS lists '${stale}' but no skills/${stale}.md exists`);
184
183
  }
185
184
 
186
- // The numerals the laws RENDER with — positional, off the canon — because LAW_CLASS and the
187
- // console's LAW_META are keyed the way the rendered catalogue numbers them.
185
+ // The numerals the laws RENDER with — positional, off the canon — because LAW_CLASS is keyed
186
+ // the way the rendered catalogue numbers them.
188
187
  const canonRules = ruleCanon(SRC).rules;
189
- const lawRules = canonRules.filter((r) => r.kind === 'LAW');
190
- const lawNums = lawRules.map((r) => r.label);
188
+ const lawNums = canonRules.filter((r) => r.kind === 'LAW').map((r) => r.label);
191
189
  for (const num of lawNums) {
192
190
  if (!has(LAW_CLASS, num)) {
193
191
  problems.push(`[authoring] laws/universal.md rule ${num} has no class in LAW_CLASS`);
@@ -200,7 +198,6 @@ export function countTableProblems() {
200
198
  + `class ${formatRepr(LAW_CLASSES)}`);
201
199
  }
202
200
  }
203
- problems.push(...lawMetaProblems(lawRules, LAW_CLASS, LAW_CLASSES));
204
201
 
205
202
  // WHAT SHIPS, NOT WHAT IS FLAT. A folder skill (`src/skills/<name>/SKILL.md`, vendored with
206
203
  // its scripts or references) ships exactly like a flat one, so the badge, the README list and
@@ -24,7 +24,7 @@ import { readText, withDiscardableStderr, isDir, isFile } from '../lib/fs.mjs';
24
24
  import { comparePaths, normcase, within } from '../lib/paths.mjs';
25
25
  import { stripWhitespace } from '../lib/text.mjs';
26
26
  import { readJsonc } from '../hosts/settings.mjs';
27
- import { loadKits, makeCfg } from '../build/source.mjs';
27
+ import { SRC, loadKits, makeCfg, ruleCanon, ruleSummaries } from '../build/source.mjs';
28
28
  import { loadTheme } from '../build/render.mjs';
29
29
 
30
30
  /** A requested catalog section or item that does not exist. */
@@ -427,8 +427,7 @@ function configManifest(name, p) {
427
427
  *
428
428
  * ⚠ `laws` STAYS ONE SECTION FOR ALL THREE TIERS, and that is a decision rather than an
429
429
  * oversight. The console has ONE Constitution entry, not three, so a reader keeps the "read it
430
- * top to bottom" property; the id stays `laws` because `tests/helpers/cli_golden.mjs`
431
- * hard-requires `web/src/pages/Laws.jsx` and because a route rename buys nothing and costs a
430
+ * top to bottom" property; the id stays `laws` because a route rename buys nothing and costs a
432
431
  * redirect. The tier lives on the ITEM (`tier: 'ontology' | 'invariant' | 'doctrine'`), which
433
432
  * is where a consumer can act on it.
434
433
  */
@@ -445,17 +444,25 @@ export const SECTIONS = ['agents', 'skills', 'laws', 'memory', 'notebook', 'wiki
445
444
  * Names are ADDRESSES and the three shapes cannot collide: `ont:<slug>` carries a colon no
446
445
  * numeral has, an invariant is `[IVXLCDM]+`, a doctrine rule is `<pack>.<n>`. `apiItem`
447
446
  * resolves all three off the same list.
447
+ *
448
+ * `summary` is the rule's one-line Principle, read from this checkout's `src/` and joined by the
449
+ * address the canon renders now (`ruleSummaries`, `js/build/source.mjs`). `desc` stays empty:
450
+ * the search index reads `desc`, and the summary is display copy, not a description to match.
448
451
  */
449
452
  function constitutionItems(inv) {
453
+ const said = ruleSummaries(SRC);
454
+ const summary = new Map(ruleCanon(SRC).rules.map((r) => [
455
+ r.kind === 'LAW' ? r.label : `${r.pack}.${r.n}`, said.get(r.id) ?? '']));
450
456
  return [
451
457
  ...(inv.ontology ?? []).map((e) => ({ name: `ont:${e.id}`, title: e.title, desc: '',
452
458
  tier: 'ontology' })),
453
459
  ...inv.laws.map((e) => ({ name: e.num, title: `Rule ${e.num} — ${e.title}`, desc: '',
454
- klass: e.klass ?? 'craft', tier: 'invariant' })),
460
+ summary: summary.get(e.num) ?? '', klass: e.klass ?? 'craft', tier: 'invariant' })),
455
461
  ...(inv.doctrines ?? []).flatMap((p) => p.rules.map((r) => ({
456
462
  name: `${r.pack}.${r.n}`,
457
463
  title: `Doctrine ${r.pack} ${r.n} — ${r.title}`,
458
464
  desc: '',
465
+ summary: summary.get(`${r.pack}.${r.n}`) ?? '',
459
466
  klass: r.klass ?? p.pack,
460
467
  tier: 'doctrine',
461
468
  pack: p.pack,
@@ -195,7 +195,7 @@ function ruleBlock(rid, title, scope, source, trialUntil, body) {
195
195
  *
196
196
  * REFUSED, NEVER REWRITTEN: a body line that `isRuleEdge` reads would truncate this rule and
197
197
  * could forge another rule's id, and a `|` in the scope or source would forge a metadata field. An
198
- * unknown scope (`global`, written by hand) is kept as written — coercing it to `project` made
198
+ * unknown scope (one written by hand, such as `team`) is kept as written — coercing it to `project` made
199
199
  * every Edit or Graduate silently change the user's rule.
200
200
  */
201
201
  function ruleFields(body) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "//": "Every field here is argued and gated in tests/unit/package_manifest.test.mjs. `files`, `bin` and `engines` each have a test that fails when reality drifts from this file; `version` mirrors harness.config.json and is gated too. Read that docblock before editing this one.",
3
3
  "name": "geneseed",
4
- "version": "4.18.1",
4
+ "version": "4.19.0",
5
5
  "description": "A portable, theme-able harness you implant once and use everywhere to grow a disciplined AI coding agent.",
6
6
  "keywords": [
7
7
  "ai",
@@ -44,7 +44,6 @@
44
44
  "docs/concepts/",
45
45
  "docs/reference/",
46
46
  "web/dist/",
47
- "web/src/pages/Laws.jsx",
48
47
  "harness.config.json",
49
48
  "registry.json",
50
49
  "geneseed",