geneseed 2.0.0 → 3.0.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 +217 -5
- package/DESIGN.md +149 -1326
- package/QUICKSTART.md +1 -1
- package/README.md +30 -22
- package/SETUP.md +98 -24
- package/SHIPPED.md +6 -5
- package/adapters/bob/README.md +3 -3
- package/adapters/claude-code/README.md +6 -4
- package/adapters/claude-code/skills/geneseed/SKILL.md +10 -6
- package/adapters/copilot/README.md +5 -4
- package/adapters/opencode/GLOBAL-HARNESS-SPEC.md +11 -6
- package/adapters/opencode/HOW-OPENCODE-LOADS.md +1 -1
- package/adapters/opencode/README.md +15 -9
- package/adapters/opencode/agents/orchestrator.md +1 -1
- package/adapters/opencode/opencode.json +0 -9
- package/adapters/opencode/plugins/geneseed-context.js +7 -7
- package/adapters/opencode/plugins/geneseed-guard.js +6 -6
- package/adapters/opencode/workflows/dispatch.js +1 -1
- package/bin/geneseed.mjs +74 -8
- package/docs/declined.md +53 -2
- package/docs/design-history.md +1290 -0
- package/docs/extending.md +672 -0
- package/docs/limits.md +141 -82
- package/docs/token-footprint.md +15 -4
- package/docs/web/agents.md +1 -1
- package/docs/web/authoring.md +1 -1
- package/docs/web/claude-hooks.md +2 -2
- package/docs/web/collaboration.md +3 -3
- package/docs/web/first-session.md +2 -2
- package/docs/web/footprint.md +14 -7
- package/docs/web/foreman-mode.md +2 -2
- package/docs/web/harnesses.md +5 -3
- package/docs/web/install-quick.md +1 -1
- package/docs/web/model.md +3 -3
- package/docs/web/plugin-context.md +2 -2
- package/docs/web/plugin-guard.md +3 -3
- package/docs/web/plugin-ponytail.md +1 -1
- package/docs/web/rules.md +23 -3
- package/docs/web/setup-choices.md +20 -7
- package/docs/web/themes-author.md +13 -1
- package/docs/web-ui.md +36 -12
- package/geneseed +0 -0
- package/harness.config.json +1 -1
- package/js/catalog.mjs +29 -4
- package/js/checkout.mjs +24 -1
- package/js/cli-table.json +9 -7
- package/js/diff.mjs +8 -3
- package/js/doctor.mjs +374 -26
- package/js/emit.mjs +48 -11
- package/js/generate.mjs +39 -5
- package/js/hooks.mjs +8 -7
- package/js/installs.mjs +101 -4
- package/js/inventory.mjs +150 -20
- package/js/lib/fs.mjs +6 -2
- package/js/mcp.mjs +3 -15
- package/js/migrate.mjs +10 -4
- package/js/render.mjs +116 -11
- package/js/settings.mjs +297 -28
- package/js/setup.mjs +95 -5
- package/js/status.mjs +45 -4
- package/js/themes.mjs +1 -1
- package/js/tui.mjs +46 -3
- package/js/uninstall.mjs +30 -11
- package/js/web/actions.mjs +76 -9
- package/js/web/api.mjs +95 -16
- package/js/web/docs.mjs +33 -5
- package/js/web/jobs.mjs +12 -3
- package/js/web/server.mjs +59 -2
- package/package.json +1 -1
- package/src/AGENT.md.tmpl +75 -65
- package/src/agents/_template.md +4 -4
- package/src/agents/advocate.md +4 -4
- package/src/agents/architect.md +2 -2
- package/src/agents/developer.md +5 -5
- package/src/agents/docs.md +2 -2
- package/src/agents/empiricist.md +3 -3
- package/src/agents/explorer.md +3 -3
- 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/reviewer.md +2 -2
- package/src/agents/security.md +2 -2
- package/src/agents/skeptic.md +4 -4
- package/src/agents/steward.md +3 -3
- package/src/agents/tester.md +2 -2
- package/src/agents/user-advocate.md +3 -3
- package/src/agents/visionary.md +3 -3
- package/src/doctrines/README.md +41 -0
- package/src/doctrines/craft.md +51 -0
- package/src/doctrines/ops.md +61 -0
- package/src/doctrines/process.md +87 -0
- package/src/doctrines/rigor.md +52 -0
- package/src/laws/universal.md +17 -301
- package/src/memory/README.md +2 -2
- package/src/modes/README.md +2 -2
- package/src/modes/foreman.md +1 -1
- package/src/notebook/README.md +2 -2
- package/src/ontology/universal.md +89 -0
- package/src/postures/README.md +3 -3
- package/src/postures/artisan.md +3 -3
- package/src/postures/assistant.md +1 -1
- package/src/postures/expert.md +1 -1
- package/src/postures/mentor.md +1 -1
- package/src/postures/peer.md +2 -2
- package/src/skills/_template.md +2 -2
- package/src/skills/brainstorm.md +2 -2
- package/src/skills/clarify.md +2 -2
- package/src/skills/codebase-design.md +1 -1
- package/src/skills/commit.md +5 -5
- package/src/skills/council.md +4 -4
- package/src/skills/crash-course.md +1 -1
- package/src/skills/daydream/SKILL.md +1 -1
- package/src/skills/debug.md +2 -2
- package/src/skills/decode.md +1 -1
- package/src/skills/develop.md +6 -6
- package/src/skills/document-project.md +4 -4
- package/src/skills/domain-modeling.md +1 -1
- package/src/skills/drill.md +1 -1
- package/src/skills/feynman.md +1 -1
- package/src/skills/forge-mcp.md +3 -3
- package/src/skills/fresh-eyes.md +4 -4
- package/src/skills/frontend-design.md +1 -1
- package/src/skills/gap-detector.md +1 -1
- package/src/skills/geneseed-code-review.md +1 -1
- package/src/skills/geneseed.md +4 -4
- package/src/skills/git-archaeology.md +1 -1
- package/src/skills/git-rescue.md +3 -3
- package/src/skills/handoff.md +4 -4
- package/src/skills/herdr.md +1 -1
- package/src/skills/ingest.md +3 -3
- package/src/skills/learning-path.md +1 -1
- package/src/skills/migrate.md +5 -5
- package/src/skills/opencode-theme.md +1 -1
- package/src/skills/parallel-agents.md +2 -2
- package/src/skills/pipeline.md +2 -2
- package/src/skills/plan.md +2 -2
- package/src/skills/ponytail.md +1 -1
- package/src/skills/profile.md +1 -1
- package/src/skills/prose.md +2 -2
- package/src/skills/prototype.md +2 -2
- package/src/skills/refactor.md +2 -2
- package/src/skills/release.md +4 -4
- package/src/skills/repo-map.md +5 -5
- package/src/skills/research.md +6 -6
- package/src/skills/review-response.md +3 -3
- package/src/skills/roast-me.md +1 -1
- package/src/skills/rule.md +5 -5
- package/src/skills/ship.md +3 -3
- package/src/skills/tdd.md +2 -2
- package/src/skills/tickets.md +1 -1
- package/src/skills/wayfinder.md +1 -1
- package/src/skills/wiki.md +4 -4
- package/src/skills/workflow.md +2 -2
- package/themes/README.md +17 -7
- package/themes/_TEMPLATE.json +37 -33
- package/themes/biker.json +37 -33
- package/themes/commentator.json +37 -33
- package/themes/cyberpunk.json +37 -33
- package/themes/gamer.json +37 -33
- package/themes/imperial.json +37 -33
- package/themes/joker.json +37 -33
- package/themes/marvin.json +37 -33
- package/themes/mean.json +37 -33
- package/themes/military.json +37 -33
- package/themes/neutral.json +37 -33
- package/themes/pirate.json +37 -33
- package/themes/sports.json +37 -33
- package/themes/verstappen.json +37 -33
- package/themes/wizard.json +37 -33
- package/web/dist/assets/{About-DX1-VTBP.js → About-C0Mk0igT.js} +1 -1
- package/web/dist/assets/{Activity-Dp9NaXOn.js → Activity-C4aJJVXz.js} +1 -1
- package/web/dist/assets/{ActivityDetail-HpOBtrXS.js → ActivityDetail-DaQ85za0.js} +1 -1
- package/web/dist/assets/{Diff-Ct4G3Se8.js → Diff-B3tlO2Yj.js} +1 -1
- package/web/dist/assets/{Doctor-DGjkZFuA.js → Doctor-BNC-5Dmo.js} +1 -1
- package/web/dist/assets/{Graph-b8o-S6kv.js → Graph-us0T6ZcV.js} +1 -1
- package/web/dist/assets/Harnesses-DAMgyfM_.js +1 -0
- package/web/dist/assets/Laws-rOxegJ75.js +1 -0
- package/web/dist/assets/{Library-BEZscyaQ.js → Library-KmwrHA1J.js} +1 -1
- package/web/dist/assets/{Markdown-k_LSLBGq.js → Markdown-0eQOVGSF.js} +1 -1
- package/web/dist/assets/{Profile-DydukTxP.js → Profile-ZU7WV6Xl.js} +1 -1
- package/web/dist/assets/{Rules-CIRG4Ldt.js → Rules-CtDoPhhf.js} +1 -1
- package/web/dist/assets/{Skills-Buoi1IBr.js → Skills-BhoOxdkD.js} +1 -1
- package/web/dist/assets/{StatusBadge-CE_AJttl.js → StatusBadge-CGvYVCwa.js} +1 -1
- package/web/dist/assets/{Themes-CI4o3gJC.js → Themes-Dr7OOkD7.js} +1 -1
- package/web/dist/assets/{activity-BCW7e6Cy.js → activity-B8aq3qtl.js} +1 -1
- package/web/dist/assets/index-2wqPDxuN.css +1 -0
- package/web/dist/assets/{index-B1PIx7RM.js → index-AhMzkdbI.js} +1 -1
- package/web/dist/assets/index-CRftEtBX.js +12 -0
- package/web/dist/assets/{index-Ck2iCdaC.js → index-D0lPN_qW.js} +1 -1
- package/web/dist/index.html +2 -2
- package/web/src/pages/Laws.jsx +282 -88
- package/web/dist/assets/Harnesses-gr3-aHBF.js +0 -1
- package/web/dist/assets/Laws-DTwF5tTC.js +0 -1
- package/web/dist/assets/index-BnTQUSUz.js +0 -12
- package/web/dist/assets/index-DUCsDv7z.css +0 -1
package/js/doctor.mjs
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `_harness_build.cmd_doctor` — validate the build.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
4
|
+
* Fifteen `*Problems` checks over one theme (or every theme) — and `[doctor] ok — …` when all
|
|
5
|
+
* fifteen find nothing. That last sentence is why this module's gate looks the way it does, and
|
|
6
|
+
* it is worth stating here rather than only in the spec: **delete any single check
|
|
7
|
+
* and a clean run is byte-identical**, so a comparison of two runs cannot tell this
|
|
8
|
+
* file from a stub that prints the OK line. `tests/unit/harness.test.mjs` and
|
|
9
|
+
* `tests/unit/authoring_gates.test.mjs` therefore plant ONE FAULT PER CHECK, in a private copy
|
|
10
|
+
* of the checkout — see `copyCheckout` in `tests/helpers/cli_golden.mjs` for why a verb that
|
|
11
|
+
* reads `ROOT` needed a fixture kind that did not exist until the port.
|
|
12
12
|
*
|
|
13
13
|
* WHY THIS IS THE VERB THAT SPAWNS. `bin/geneseed-cli.mjs` was under a blanket transitive
|
|
14
14
|
* `child_process` ban, and its own docblock predicted the day a verb that genuinely spawns
|
|
@@ -33,7 +33,9 @@ import { spawnSync } from 'node:child_process';
|
|
|
33
33
|
import os from 'node:os';
|
|
34
34
|
import path from 'node:path';
|
|
35
35
|
|
|
36
|
-
import {
|
|
36
|
+
import {
|
|
37
|
+
CONFIG, PACK_ORDER, PLUGIN_SRC, ROOT, SRC, THEMES, WORKFLOW_SRC, makeCfg,
|
|
38
|
+
} from './checkout.mjs';
|
|
37
39
|
import {
|
|
38
40
|
buildInto, main as driverMain, emitGlobalInto, emitProjectInto,
|
|
39
41
|
} from '../bin/geneseed.mjs';
|
|
@@ -48,7 +50,7 @@ import {
|
|
|
48
50
|
} from './native.mjs';
|
|
49
51
|
import { PALETTE_ROLES, colorThemeFiles } from './opencode.mjs';
|
|
50
52
|
import { STRUCTURE, renderAll } from './render.mjs';
|
|
51
|
-
import {
|
|
53
|
+
import { hookShimPath, shimDeadPaths } from './settings.mjs';
|
|
52
54
|
import { pySplitLines } from './lib/udiff.mjs';
|
|
53
55
|
import { NO_WINDOW } from './lib/proc.mjs';
|
|
54
56
|
import {
|
|
@@ -61,7 +63,10 @@ import {
|
|
|
61
63
|
// --------------------------------------------------------------------------------------
|
|
62
64
|
|
|
63
65
|
/** `_harness_core.TOKEN_RE` / `LINK_RE` / `ABS_LINK_RE`. `g` where `findall` is used. */
|
|
64
|
-
|
|
66
|
+
// Must stay in step with `js/render.mjs`'s TOKEN_RE: a token the renderer substitutes but this
|
|
67
|
+
// scan cannot see is an unresolved-token gate that silently stops gating. Digits are legal after
|
|
68
|
+
// the first character for `DOC_<PACK>_<n>`; a leading digit is still not a token.
|
|
69
|
+
const TOKEN_RE = /\{\{[A-Z_][A-Z0-9_]*\}\}/g;
|
|
65
70
|
const LINK_RE = /\]\((?!https?:\/\/|#)([^)]+)\)/g;
|
|
66
71
|
const ABS_LINK_RE = /^([A-Za-z]:[\\/]|\/|~)/;
|
|
67
72
|
const FENCE_RE = /```[\s\S]*?```/g;
|
|
@@ -516,9 +521,9 @@ export function shimProblems() {
|
|
|
516
521
|
return [`[shim] ${p} exists but cannot be read (${e.message}) — hooks may be dead`];
|
|
517
522
|
}
|
|
518
523
|
// The body is machine-generated and quotes the two baked paths — plus, on POSIX, the argv
|
|
519
|
-
// placeholder `"$@"`, which is not a path. `
|
|
520
|
-
|
|
521
|
-
return
|
|
524
|
+
// placeholder `"$@"`, which is not a path. `shimDeadPaths` is the single owner of that rule;
|
|
525
|
+
// `hookPrefix` is its other reader, and the two must not drift.
|
|
526
|
+
return shimDeadPaths(body).map(
|
|
522
527
|
(m) => `[shim] ${p} pointed at ${m}, which does not exist — every hook in every `
|
|
523
528
|
+ "install was dead (the checkout most likely moved). This run's own "
|
|
524
529
|
+ 'emit has refreshed it; no further action needed.',
|
|
@@ -660,6 +665,63 @@ export function lawMetaProblems(lawNums, lawClass, lawClasses) {
|
|
|
660
665
|
return problems;
|
|
661
666
|
}
|
|
662
667
|
|
|
668
|
+
/** `LAW_META_ROW`'s twin for a STRING key — `'<pack>.<n>': ['<pack>', '<principle>'],`. */
|
|
669
|
+
const DOCTRINE_META_ROW =
|
|
670
|
+
/^[^\S\n]*(['"])([a-z]+\.\d+)\1:\s*\[\s*(['"])(.*?)\3\s*,\s*(['"])(.*?)\5\s*,?\s*\]\s*,?[^\S\n]*$/gm;
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* `lawMetaProblems`' doctrine half — the console's per-rule Principle column stays complete.
|
|
674
|
+
*
|
|
675
|
+
* SAME DEFECT, ONE TIER OVER. `DOCTRINE_META` holds copy that exists nowhere else in the tree,
|
|
676
|
+
* and a rule with no row falls back to an empty principle: it renders with a blank description
|
|
677
|
+
* while every count upstream reads correct. Two invariants shipped exactly that way before
|
|
678
|
+
* `lawMetaProblems` existed, and 23 new rules is a much larger surface to lose one in.
|
|
679
|
+
*
|
|
680
|
+
* ⚠ THE CLASS ARM IS DIFFERENT FROM THE INVARIANTS'. An invariant's class is checked against
|
|
681
|
+
* `LAW_CLASSES`, a vocabulary of six; a doctrine rule has no second taxonomy — its class IS its
|
|
682
|
+
* pack — so the check is an EQUALITY against the pack in its own key. That catches the copy
|
|
683
|
+
* error a six-way vocabulary cannot: a row pasted from the pack above it, keeping the class of
|
|
684
|
+
* the pack it came from, which would colour the row and label its chip with the wrong pack.
|
|
685
|
+
*/
|
|
686
|
+
export function doctrineMetaProblems(addrs) {
|
|
687
|
+
const page = path.join(ROOT, 'web', 'src', 'pages', 'Laws.jsx');
|
|
688
|
+
let text;
|
|
689
|
+
try { text = readText(page); } catch (e) {
|
|
690
|
+
return [`[authoring] web/src/pages/Laws.jsx unreadable: ${e.message}`];
|
|
691
|
+
}
|
|
692
|
+
const block = /^const DOCTRINE_META = \{$([\s\S]*?)^\}$/m.exec(text);
|
|
693
|
+
if (!block) {
|
|
694
|
+
return ['[authoring] DOCTRINE_META literal not found in web/src/pages/Laws.jsx — the '
|
|
695
|
+
+ "console's doctrine rows would have no gate on their Principle column"];
|
|
696
|
+
}
|
|
697
|
+
const meta = new Map();
|
|
698
|
+
for (const m of block[1].matchAll(DOCTRINE_META_ROW)) meta.set(m[2], [m[4], m[6]]);
|
|
699
|
+
const problems = [];
|
|
700
|
+
const seen = new Set();
|
|
701
|
+
for (const addr of addrs) {
|
|
702
|
+
seen.add(addr);
|
|
703
|
+
if (!meta.has(addr)) {
|
|
704
|
+
problems.push(`[authoring] doctrine rule ${addr} has no row in DOCTRINE_META `
|
|
705
|
+
+ '(web/src/pages/Laws.jsx) — the console would render it with a blank Principle');
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
const [klass, principle] = meta.get(addr);
|
|
709
|
+
if (!principle.trim()) {
|
|
710
|
+
problems.push(`[authoring] DOCTRINE_META['${addr}'] has an empty principle line — that `
|
|
711
|
+
+ 'rule would render with a blank description');
|
|
712
|
+
}
|
|
713
|
+
const pack = addr.split('.')[0];
|
|
714
|
+
if (klass !== pack) {
|
|
715
|
+
problems.push(`[authoring] DOCTRINE_META['${addr}'] carries pack '${klass}' — a doctrine `
|
|
716
|
+
+ `rule's class IS its pack, so this must read '${pack}'`);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
for (const addr of [...meta.keys()].filter((k) => !seen.has(k)).sort()) {
|
|
720
|
+
problems.push(`[authoring] DOCTRINE_META lists ${addr} but no pack file defines it`);
|
|
721
|
+
}
|
|
722
|
+
return problems;
|
|
723
|
+
}
|
|
724
|
+
|
|
663
725
|
/**
|
|
664
726
|
* `_harness_build._prose_mirror_problems` — the human-readable count mirrors, the ones the
|
|
665
727
|
* badge regex never sees.
|
|
@@ -736,6 +798,14 @@ export function proseMirrorProblems(readme, web, counts, skillStems, shipped = '
|
|
|
736
798
|
}
|
|
737
799
|
}
|
|
738
800
|
|
|
801
|
+
// ⚠ THE ABSENCE OF THIS TRIPLE IS CHECKED BY THE CALLER, NOT HERE, AND THAT IS FORCED RATHER
|
|
802
|
+
// THAN CHOSEN. This arm is satisfiable by DELETING THE SENTENCE — no match, no problem, green
|
|
803
|
+
// — which is a gate failing open. The obvious fix is an `else` right here, and it cannot go
|
|
804
|
+
// here: `prose_mirror_problems` is one of the pure functions recorded in
|
|
805
|
+
// `tests/__snapshots__/primitives/`, the recording holds a case that passes an EMPTY `shipped`
|
|
806
|
+
// and expects `[]`, and there is no recorder left to re-bless it. So the presence check lives
|
|
807
|
+
// in `countTableProblems`, which is not recorded — beside the read that already knows whether
|
|
808
|
+
// the file could be opened at all, which is where it reads better anyway.
|
|
739
809
|
const s = /(\d+) laws, (\d+) agents, (\d+) skills/.exec(shipped);
|
|
740
810
|
if (s) {
|
|
741
811
|
for (const [got, want, label] of [[s[1], laws, 'laws'], [s[2], agents, 'agents'],
|
|
@@ -752,6 +822,237 @@ export function proseMirrorProblems(readme, web, counts, skillStems, shipped = '
|
|
|
752
822
|
const TMPL_SPEC_RE = /\{\{DIR_(AGENTS|SKILLS)\}\}\/([A-Za-z0-9_-]+)\.md/g;
|
|
753
823
|
const LAW_HEADING_RE = /^### \{\{LAW\}\} ([IVXLCDM]+)\b/gm;
|
|
754
824
|
|
|
825
|
+
// The doctrine twins of `LAW_HEADING_RE`, over the UNRENDERED source — `js/inventory.mjs`'s
|
|
826
|
+
// pair of the same names read the rendered form. A citation is the heading minus its `### `,
|
|
827
|
+
// so one regex cannot serve both: a heading is an anchored line, a citation is anywhere.
|
|
828
|
+
const DOCTRINE_HEADING_RE = /^### \{\{DOCTRINE\}\} ([a-z]+) (\d+)\b/gm;
|
|
829
|
+
const DOCTRINE_CITE_RE = /\{\{DOCTRINE\}\}\s+([a-z]+)\s+(\d+)/g;
|
|
830
|
+
// `\{\{DOCTRINE\}\}` and not `DOCTRINE`: `{{DOCTRINES}}` is the SECTION noun and is legal
|
|
831
|
+
// everywhere, including in an invariant that points at the tier as a whole.
|
|
832
|
+
const DOCTRINE_TOKEN_RE = /\{\{DOCTRINE\}\}/;
|
|
833
|
+
const DOC_TOKEN_RE = /\{\{(DOC_[A-Z0-9_]+)\}\}/g;
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* A line reported by the doctor that is NOT a failure — `[note] …` rather than `[authoring] …`.
|
|
837
|
+
*
|
|
838
|
+
* ⚠ ONE PRODUCER, AND IT IS THE ONLY THING THAT NEEDED A SECOND CHANNEL. A build with a
|
|
839
|
+
* doctrine pack off is LEGAL, and it leaves body prose citing rules that pack owns. That state
|
|
840
|
+
* has to be visible — a silent dangling citation is how prose and boundary drift apart — but
|
|
841
|
+
* reporting it as a problem would make `doctor` exit 1 over a configuration its owner chose.
|
|
842
|
+
* So `cmdDoctor` prints notes and does not count them.
|
|
843
|
+
*/
|
|
844
|
+
const NOTE = '[note] ';
|
|
845
|
+
export const isDoctorNote = (p) => p.startsWith(NOTE);
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* The three-tier constitution's own authoring gates — the doctrine twin of the `LAW_CLASS` /
|
|
849
|
+
* `LAW_META` family, plus the two purity rules the tiering rests on.
|
|
850
|
+
*
|
|
851
|
+
* IT LIVES BESIDE `countTableProblems` RATHER THAN INSIDE IT, and is called from
|
|
852
|
+
* `authoringProblems` beside `registry`/`secret`/`vendorPin`. `countTableProblems` opens by
|
|
853
|
+
* reading `AGENT.md.tmpl` and HARD-RETURNS if that read fails, so an arm placed inside it
|
|
854
|
+
* would go quiet on exactly the tree that is most broken. Nothing here needs the template's
|
|
855
|
+
* tables, and `proseMirrorProblems` — the frozen recording — is untouched either way.
|
|
856
|
+
*
|
|
857
|
+
* ⚠ THE VOCABULARY GATES ARE HERE BECAUSE `themeParityProblems` CANNOT SEE THEM. That check is
|
|
858
|
+
* presence-only and SYMMETRIC over the union of every theme's keys: a key missing from all
|
|
859
|
+
* fourteen is in parity, and a key whose VALUE breaks a parser is not its business at all. So
|
|
860
|
+
* `DOC_*` coverage, the `LEX_I..LEX_IX` equality and the single-word tier noun each need a gate
|
|
861
|
+
* that reads the SOURCE and asks what the source requires.
|
|
862
|
+
*/
|
|
863
|
+
export function constitutionProblems() {
|
|
864
|
+
const problems = [];
|
|
865
|
+
const dir = path.join(SRC, 'doctrines');
|
|
866
|
+
|
|
867
|
+
// ---- the packs themselves: present, contiguous from 1, and filed where they say they are.
|
|
868
|
+
const rules = new Map(); // 'craft.1' -> title-less marker
|
|
869
|
+
for (const pack of PACK_ORDER) {
|
|
870
|
+
const file = path.join(dir, `${pack}.md`);
|
|
871
|
+
let text;
|
|
872
|
+
try { text = readText(file); } catch (e) {
|
|
873
|
+
problems.push(`[authoring] doctrines/${pack}.md is named in PACK_ORDER but unreadable: `
|
|
874
|
+
+ `${e.message} — the build ships the whole catalogue, so a missing pack file breaks `
|
|
875
|
+
+ 'every citation into it, active or not');
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
const found = [...text.matchAll(DOCTRINE_HEADING_RE)];
|
|
879
|
+
if (!found.length) {
|
|
880
|
+
problems.push(`[authoring] doctrines/${pack}.md carries no '### {{DOCTRINE}} ${pack} <n>' `
|
|
881
|
+
+ 'heading — the pack would render as an empty section');
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
found.forEach(([, named, n], i) => {
|
|
885
|
+
if (named !== pack) {
|
|
886
|
+
problems.push(`[authoring] doctrines/${pack}.md holds a rule addressed to '${named} `
|
|
887
|
+
+ `${n}' — a rule's pack is read from its heading, so this one is unreachable at `
|
|
888
|
+
+ `'${pack}.${n}' and shadows whatever ${named}.md numbers ${n}`);
|
|
889
|
+
}
|
|
890
|
+
if (Number(n) !== i + 1) {
|
|
891
|
+
problems.push(`[authoring] doctrines/${pack}.md rule ${n} is at position ${i + 1} — `
|
|
892
|
+
+ 'pack ids must run contiguously from 1, or a citation addresses a gap');
|
|
893
|
+
}
|
|
894
|
+
rules.set(`${named}.${Number(n)}`, true);
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// The console's Principle column, keyed off the rules just parsed — so the gate is driven by
|
|
899
|
+
// the SOURCE and a rule added to a pack file is caught the same day, not when someone
|
|
900
|
+
// notices a blank description in the browser.
|
|
901
|
+
problems.push(...doctrineMetaProblems([...rules.keys()]));
|
|
902
|
+
|
|
903
|
+
// ---- every `{{DOCTRINE}} <pack> <n>` anywhere in src/ resolves to a rule that exists.
|
|
904
|
+
//
|
|
905
|
+
// The whole tree, not the template: a skill footer citing `process 9` is as broken as a
|
|
906
|
+
// template one, and it is the shape 339 rewired citations could have left behind.
|
|
907
|
+
const cited = new Map(); // 'process.5' -> [rel, …]
|
|
908
|
+
for (const p of rglob(SRC)) {
|
|
909
|
+
if (!isFile(p) || !(p.endsWith('.md') || p.endsWith('.tmpl'))) continue;
|
|
910
|
+
let text;
|
|
911
|
+
try { text = readText(p); } catch { continue; }
|
|
912
|
+
const rel = path.relative(SRC, p).split(path.sep).join('/');
|
|
913
|
+
// ⚠ AN ALWAYS-ON TIER MAY NEVER CITE A TOGGLEABLE ONE (D2). A `--doctrines craft` build
|
|
914
|
+
// ships invariants that point at rules its AGENT.md does not contain; the ontology is
|
|
915
|
+
// worse still, being the tier that states the order the others sit in. Doctrine->doctrine
|
|
916
|
+
// citations are fine and stay: the full catalogue ships in every bundle.
|
|
917
|
+
if ((rel.startsWith('ontology/') || rel.startsWith('laws/')) && DOCTRINE_TOKEN_RE.test(text)) {
|
|
918
|
+
problems.push(`[authoring] ${rel} cites {{DOCTRINE}} — an always-on tier may never `
|
|
919
|
+
+ 'reference a toggleable one, or a pack-off build ships a rule pointing at text '
|
|
920
|
+
+ 'that is not there. State the point directly instead');
|
|
921
|
+
}
|
|
922
|
+
for (const [, pack, n] of text.matchAll(DOCTRINE_CITE_RE)) {
|
|
923
|
+
const key = `${pack}.${Number(n)}`;
|
|
924
|
+
if (!cited.has(key)) cited.set(key, []);
|
|
925
|
+
cited.get(key).push(rel);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
for (const [key, where] of [...cited].sort()) {
|
|
929
|
+
if (rules.has(key)) continue;
|
|
930
|
+
problems.push(`[authoring] ${where[0]} cites {{DOCTRINE}} ${key.replace('.', ' ')}, which `
|
|
931
|
+
+ `no pack file defines${where.length > 1 ? ` (and ${where.length - 1} more)` : ''}`);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// ---- the vocabulary every theme owes the source.
|
|
935
|
+
const wanted = new Set();
|
|
936
|
+
for (const pack of PACK_ORDER) {
|
|
937
|
+
let text;
|
|
938
|
+
try { text = readText(path.join(dir, `${pack}.md`)); } catch { continue; }
|
|
939
|
+
for (const [, key] of text.matchAll(DOC_TOKEN_RE)) wanted.add(key);
|
|
940
|
+
}
|
|
941
|
+
const lexWant = ['I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX'].map((r) => `LEX_${r}`);
|
|
942
|
+
// `_TEMPLATE.json` is included — it is the file `--sync-themes` seeds a new theme from, so a
|
|
943
|
+
// stale key there propagates into every voice added afterwards. `themeFiles()` excludes it.
|
|
944
|
+
const files = [...themeFiles(), path.join(THEMES, '_TEMPLATE.json')];
|
|
945
|
+
for (const f of files) {
|
|
946
|
+
const name = path.basename(f);
|
|
947
|
+
let theme;
|
|
948
|
+
try { theme = parseJson(readText(f)); } catch (e) {
|
|
949
|
+
problems.push(`[authoring] ${name} unreadable: ${e.message}`);
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
if (!theme || typeof theme !== 'object' || Array.isArray(theme)) continue;
|
|
953
|
+
const keys = Object.keys(theme);
|
|
954
|
+
for (const key of [...wanted].sort()) {
|
|
955
|
+
if (!Object.hasOwn(theme, key)) {
|
|
956
|
+
problems.push(`[authoring] ${name} has no {${key}} — a doctrine file names it, and `
|
|
957
|
+
+ 'theme parity cannot see a key that is missing from every theme at once');
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
for (const key of keys.filter((k) => k.startsWith('DOC_')).sort()) {
|
|
961
|
+
if (!wanted.has(key)) {
|
|
962
|
+
problems.push(`[authoring] ${name} defines {${key}}, which no doctrine file uses — a `
|
|
963
|
+
+ 'dead voice key ships a title for a rule that does not exist');
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
// I1 — an EQUALITY, not a presence check. The renumber left `LEX_XXII`, `LEX_XXIII`,
|
|
967
|
+
// `LEX_XXIV` and `LEX_XXXVI` behind in all fifteen files, and parity was silent because
|
|
968
|
+
// they were absent from nowhere.
|
|
969
|
+
const lex = keys.filter((k) => k.startsWith('LEX_')).sort();
|
|
970
|
+
const stale = lex.filter((k) => !lexWant.includes(k));
|
|
971
|
+
const missing = lexWant.filter((k) => !lex.includes(k));
|
|
972
|
+
for (const k of stale) {
|
|
973
|
+
problems.push(`[authoring] ${name} still defines {${k}} — the invariants are I..IX and `
|
|
974
|
+
+ 'nothing renders this key, so it is a title for a rule that no longer exists');
|
|
975
|
+
}
|
|
976
|
+
for (const k of missing) {
|
|
977
|
+
problems.push(`[authoring] ${name} has no {${k}} — one of the nine invariants would `
|
|
978
|
+
+ 'render with an empty title');
|
|
979
|
+
}
|
|
980
|
+
// M10 — ⚠ BOTH HEADING PARSERS MATCH THE TIER NOUN WITH `\S+`. A two-word value does not
|
|
981
|
+
// error; the heading stops matching and the tier silently parses to nothing. `_TEMPLATE`'s
|
|
982
|
+
// values are descriptive placeholders, so only real voices are held to this.
|
|
983
|
+
if (name === '_TEMPLATE.json') continue;
|
|
984
|
+
for (const key of ['LAW', 'DOCTRINE']) {
|
|
985
|
+
const v = theme[key];
|
|
986
|
+
if (typeof v === 'string' && /\s/.test(v)) {
|
|
987
|
+
problems.push(`[authoring] ${name} spells {${key}} as ${pyRepr(v)} — the heading `
|
|
988
|
+
+ 'parsers match the tier noun with \\S+, so a value with a space makes every '
|
|
989
|
+
+ `${key === 'LAW' ? 'invariant' : 'doctrine rule'} parse to nothing, in silence`);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// ---- every `§N` in src/ addresses a section the template actually declares.
|
|
995
|
+
//
|
|
996
|
+
// ⚠ THIS GATE IS NARROW AND SAYS SO. Inserting `## 2. Doctrines` pushed every later section
|
|
997
|
+
// down one, and the sweep that fixed the template's own cross-references stopped at the
|
|
998
|
+
// template — four satellites under `src/skills/` and `src/agents/` kept pointing at the
|
|
999
|
+
// section that used to be there, and they RENDER INTO EVERY BUNDLE. A range check would not
|
|
1000
|
+
// have caught those: §7 still existed, it had just stopped being the Wiki.
|
|
1001
|
+
//
|
|
1002
|
+
// What it does catch is the other half of a renumber — a pointer past the end, which is what
|
|
1003
|
+
// REMOVING a section leaves behind — and it is nearly free. The half it cannot catch is a
|
|
1004
|
+
// judgement about intent, and `docs/extending.md` carries the manual sweep for it. Naming
|
|
1005
|
+
// that limit here is the point: a reader who assumes this gate is total will skip the sweep.
|
|
1006
|
+
const declared = new Set();
|
|
1007
|
+
// Read here rather than borrowing `countTableProblems`' copy: that function hard-returns when
|
|
1008
|
+
// the template is unreadable, and this gate must not inherit a silence it did not choose.
|
|
1009
|
+
let tmpl = '';
|
|
1010
|
+
try { tmpl = readText(path.join(SRC, 'AGENT.md.tmpl')); } catch { tmpl = ''; }
|
|
1011
|
+
for (const [, n] of tmpl.matchAll(/^## (\d+)\.\s/gm)) declared.add(Number(n));
|
|
1012
|
+
if (declared.size) {
|
|
1013
|
+
for (const p of rglob(SRC)) {
|
|
1014
|
+
if (!isFile(p) || !(p.endsWith('.md') || p.endsWith('.tmpl'))) continue;
|
|
1015
|
+
let text;
|
|
1016
|
+
try { text = readText(p); } catch { continue; }
|
|
1017
|
+
const rel = path.relative(SRC, p).split(path.sep).join('/');
|
|
1018
|
+
for (const [, n] of text.matchAll(/§(\d+)/g)) {
|
|
1019
|
+
if (declared.has(Number(n))) continue;
|
|
1020
|
+
problems.push(`[authoring] ${rel} cites AGENT.md §${n}, and the template declares no `
|
|
1021
|
+
+ `such section ${pyRepr([...declared].sort((a, b) => a - b).map(String))} — a `
|
|
1022
|
+
+ 'renumber moved it, and this pointer renders into every bundle');
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// ---- the build default names packs that exist.
|
|
1028
|
+
let cfgDoctrines = null;
|
|
1029
|
+
try {
|
|
1030
|
+
const cfg = parseJson(readText(CONFIG));
|
|
1031
|
+
if (cfg && typeof cfg === 'object' && Array.isArray(cfg.doctrines)) cfgDoctrines = cfg.doctrines;
|
|
1032
|
+
} catch { /* no config, or unreadable — `configDefaults()` is forgiving here too */ }
|
|
1033
|
+
if (cfgDoctrines) {
|
|
1034
|
+
for (const pack of cfgDoctrines) {
|
|
1035
|
+
if (!PACK_ORDER.includes(pack)) {
|
|
1036
|
+
problems.push(`[authoring] harness.config.json names doctrine pack ${pyRepr(pack)}, `
|
|
1037
|
+
+ `which this checkout does not ship ${pyRepr([...PACK_ORDER])} — every build reading `
|
|
1038
|
+
+ 'that default would fail');
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// ---- D5, a NOTE and not a problem: what a narrowed default leaves pointing at the shelf.
|
|
1044
|
+
const on = cfgDoctrines ?? [...PACK_ORDER];
|
|
1045
|
+
const dangling = [...cited].filter(([k]) => rules.has(k) && !on.includes(k.split('.')[0]));
|
|
1046
|
+
if (dangling.length) {
|
|
1047
|
+
const packs = [...new Set(dangling.map(([k]) => k.split('.')[0]))].sort();
|
|
1048
|
+
problems.push(`${NOTE}harness.config.json builds ${pyRepr(on)}, and ${dangling.length} `
|
|
1049
|
+
+ `citation(s) in src/ point into ${packs.join(', ')}. Those rules still SHIP — every `
|
|
1050
|
+
+ 'pack file is in the bundle — so the reference resolves on disk; it is the rendered '
|
|
1051
|
+
+ 'AGENT.md that will not contain them. Legal, and recorded here so it is not silent');
|
|
1052
|
+
}
|
|
1053
|
+
return problems;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
755
1056
|
/**
|
|
756
1057
|
* `_harness_build._count_table_problems` — the hand-authored AGENT.md tables and the README
|
|
757
1058
|
* badges, against `src/`.
|
|
@@ -821,9 +1122,19 @@ export function countTableProblems() {
|
|
|
821
1122
|
};
|
|
822
1123
|
let readme;
|
|
823
1124
|
try { readme = readText(path.join(ROOT, 'README.md')); } catch { return problems; }
|
|
1125
|
+
// ⚠ PRESENCE FIRST, THEN THE VALUE — the same absence arm the SHIPPED triple needed, for the
|
|
1126
|
+
// same reason: `if (m && …)` is green when there is no badge, so deleting a badge silences its
|
|
1127
|
+
// own gate. The `continue` keeps the loop's MESSAGE ORDER intact, which is load-bearing (see
|
|
1128
|
+
// the `plugins`-is-last comment above): a missing badge reports in its own slot rather than
|
|
1129
|
+
// reordering the ones after it.
|
|
824
1130
|
for (const [key, n] of Object.entries(counts)) {
|
|
825
1131
|
const m = new RegExp(`badge/${key}-(\\d+)`).exec(readme);
|
|
826
|
-
if (m
|
|
1132
|
+
if (!m) {
|
|
1133
|
+
problems.push(`[authoring] README has no ${key} badge — deleting one is how this gate `
|
|
1134
|
+
+ 'goes green while the reader is told nothing');
|
|
1135
|
+
continue;
|
|
1136
|
+
}
|
|
1137
|
+
if (Number(m[1]) !== n) {
|
|
827
1138
|
problems.push(`[authoring] README ${key} badge says ${m[1]} but src has ${n}`);
|
|
828
1139
|
}
|
|
829
1140
|
}
|
|
@@ -843,8 +1154,34 @@ export function countTableProblems() {
|
|
|
843
1154
|
web = globSorted(path.join(ROOT, 'docs', 'web'), (n) => n.endsWith('.md'))
|
|
844
1155
|
.map(readText).join('\n');
|
|
845
1156
|
} catch { web = ''; }
|
|
1157
|
+
// ⚠ NOT FAIL-SOFT, UNLIKE `web` ABOVE, AND THE ASYMMETRY IS THE POINT. A missing `docs/web`
|
|
1158
|
+
// tree is not an authoring fault — a checkout can legitimately lack it. SHIPPED.md is a
|
|
1159
|
+
// tracked, shipped file that this function asserts the CONTENT of, so an unreadable one is
|
|
1160
|
+
// indistinguishable from a deleted claim: swallowing it into `''` used to hand the triple arm
|
|
1161
|
+
// an empty string and turn a hard read failure into a silent pass.
|
|
1162
|
+
// ⚠ NOT FAIL-SOFT, UNLIKE `web` ABOVE, AND THE ASYMMETRY IS THE POINT. A missing `docs/web`
|
|
1163
|
+
// tree is not an authoring fault — a checkout can legitimately lack it. SHIPPED.md is a
|
|
1164
|
+
// tracked, shipped file whose CONTENT this function asserts, so an unreadable one was
|
|
1165
|
+
// indistinguishable from a deleted claim: swallowing it into `''` handed the triple arm an
|
|
1166
|
+
// empty string and turned a hard read failure into a silent pass.
|
|
1167
|
+
//
|
|
1168
|
+
// AND THE PRESENCE OF THE TRIPLE IS CHECKED HERE rather than inside `proseMirrorProblems`,
|
|
1169
|
+
// which is where it belongs by subject and cannot go by construction — that function is
|
|
1170
|
+
// recorded in `tests/__snapshots__/primitives/` with a case that passes an empty `shipped` and
|
|
1171
|
+
// expects `[]`, and nothing can re-bless a recording whose recorder is deleted. This is the
|
|
1172
|
+
// caller, it is not recorded, and it already owns the question "could the file be read".
|
|
846
1173
|
let shipped;
|
|
847
|
-
try {
|
|
1174
|
+
try {
|
|
1175
|
+
shipped = readText(path.join(ROOT, 'SHIPPED.md'));
|
|
1176
|
+
} catch (e) {
|
|
1177
|
+
problems.push(`[authoring] SHIPPED.md is unreadable (${e.message}) — the counts it carries `
|
|
1178
|
+
+ 'cannot be checked, and a gate that cannot read its subject must say so');
|
|
1179
|
+
shipped = '';
|
|
1180
|
+
}
|
|
1181
|
+
if (shipped && !/(\d+) laws, (\d+) agents, (\d+) skills/.test(shipped)) {
|
|
1182
|
+
problems.push('[authoring] SHIPPED.md carries no \'N laws, N agents, N skills\' line — '
|
|
1183
|
+
+ 'deleting it is how that gate goes green while the count drifts');
|
|
1184
|
+
}
|
|
848
1185
|
problems.push(...proseMirrorProblems(readme, web, counts, skillFiles, shipped));
|
|
849
1186
|
return problems;
|
|
850
1187
|
}
|
|
@@ -943,6 +1280,7 @@ export function authoringProblems() {
|
|
|
943
1280
|
problems.push(...registryProblems());
|
|
944
1281
|
problems.push(...secretProblems());
|
|
945
1282
|
problems.push(...vendorPinProblems());
|
|
1283
|
+
problems.push(...constitutionProblems());
|
|
946
1284
|
problems.push(...countTableProblems());
|
|
947
1285
|
return problems;
|
|
948
1286
|
}
|
|
@@ -1144,17 +1482,26 @@ function captureBoth(fn) {
|
|
|
1144
1482
|
* maintainer sweep. The cross-theme parity check runs in every mode.
|
|
1145
1483
|
*/
|
|
1146
1484
|
export function cmdDoctor(args) {
|
|
1147
|
-
const [themes,
|
|
1485
|
+
const [themes, collected] = doctorCollect({
|
|
1148
1486
|
theme: args.theme, allThemes: Boolean(args.all), bundle: args.bundle,
|
|
1149
1487
|
noBundle: Boolean(args.noBundle),
|
|
1150
1488
|
});
|
|
1151
1489
|
if (!themes.length) {
|
|
1152
|
-
pyPrint(`${
|
|
1490
|
+
pyPrint(`${collected.length ? collected[0] : '[doctor] no themes found'}\n`);
|
|
1153
1491
|
return 1;
|
|
1154
1492
|
}
|
|
1493
|
+
// ⚠ NOTES ARE PRINTED AND NOT COUNTED. The only producer is the pack-off citation report in
|
|
1494
|
+
// `constitutionProblems`, and the state it describes is one the install's owner chose: a
|
|
1495
|
+
// build that leaves body prose citing a pack it did not render. Silence there is how prose
|
|
1496
|
+
// and boundary drift apart, and a non-zero exit there is doctor failing a legal
|
|
1497
|
+
// configuration. Nothing in the recorded CLI corpus produces one — the checkout's own
|
|
1498
|
+
// `harness.config.json` names no packs — so the byte-compared output is unmoved.
|
|
1499
|
+
const notes = collected.filter(isDoctorNote);
|
|
1500
|
+
const problems = collected.filter((p) => !isDoctorNote(p));
|
|
1155
1501
|
const scoped = !args.theme && !args.all && themes.length === 1;
|
|
1156
1502
|
const note = scoped
|
|
1157
1503
|
? ` (scoped to installed theme '${themes[0]}'; run with --all to sweep every theme)` : '';
|
|
1504
|
+
for (const n of notes) pyPrint(`${n}\n`);
|
|
1158
1505
|
if (problems.length) {
|
|
1159
1506
|
pyPrint(`[doctor] ${problems.length} problem(s) across ${themes.length} theme(s):\n`);
|
|
1160
1507
|
for (const p of problems) pyPrint(` - ${p}\n`);
|
|
@@ -1163,15 +1510,16 @@ export function cmdDoctor(args) {
|
|
|
1163
1510
|
+ '`./geneseed update` (or re-sync src/), then re-check.\n');
|
|
1164
1511
|
}
|
|
1165
1512
|
if (problems.some((p) => p.startsWith('[themes]') && p.includes('missing key'))) {
|
|
1166
|
-
// `./geneseed build --sync-themes`,
|
|
1167
|
-
//
|
|
1168
|
-
//
|
|
1169
|
-
//
|
|
1170
|
-
//
|
|
1171
|
-
//
|
|
1172
|
-
//
|
|
1513
|
+
// ⚠ THE TIP USED TO NAME `./geneseed build --sync-themes`, AND THAT COMMAND ERRORS.
|
|
1514
|
+
// It was argued here on the premise that "`build` forwards its extra arguments to the
|
|
1515
|
+
// generator" — which was true of the reference and is not true of `cmdBuild`
|
|
1516
|
+
// (`js/generate.mjs`), which forwards `--theme` and nothing else. So the front door
|
|
1517
|
+
// answered `unrecognized arguments: --sync-themes` to anyone who followed doctor's own
|
|
1518
|
+
// advice, for as long as the premise went unchecked. The flag belongs to the GENERATOR,
|
|
1519
|
+
// whose binary is `geneseed-build` — which is what `README.md` and `SETUP.md` have said
|
|
1520
|
+
// all along. A hint is a command; if it is not runnable it is decoration.
|
|
1173
1521
|
pyPrint(' tip: a theme is missing a key another theme defines — run '
|
|
1174
|
-
+ '
|
|
1522
|
+
+ '`geneseed-build --sync-themes` to fill it from _TEMPLATE.json, '
|
|
1175
1523
|
+ 'then restyle the added key(s) and re-check.\n');
|
|
1176
1524
|
}
|
|
1177
1525
|
if (note) pyPrint(`${note}\n`);
|
package/js/emit.mjs
CHANGED
|
@@ -87,8 +87,29 @@ import {
|
|
|
87
87
|
} from './lib/fs.mjs';
|
|
88
88
|
/** `_build_render.SRC_DIRS_MARKER`. */
|
|
89
89
|
const SRC_DIRS_MARKER = '.geneseed-srcdirs.json';
|
|
90
|
-
/**
|
|
91
|
-
|
|
90
|
+
/**
|
|
91
|
+
* `_build_render.OWNED_SRC_DIRS` — wiped and regenerated each run.
|
|
92
|
+
*
|
|
93
|
+
* `ontology` and `doctrines` are appended, not inserted: this array's iteration order is the
|
|
94
|
+
* key order of the `.geneseed-srcdirs.json` marker it fills, and appending keeps the three
|
|
95
|
+
* original keys where an existing install's marker already has them.
|
|
96
|
+
*
|
|
97
|
+
* They belong here for the same reason `laws` does — they are GENERATED, so an install must
|
|
98
|
+
* not keep a copy the source no longer produces. Without them a build writes the dirs on
|
|
99
|
+
* every run and never wipes them, which turns the doctrines toggle into a one-way switch:
|
|
100
|
+
* disable a pack, rebuild, and its rendered file sits in the install forever, contradicting
|
|
101
|
+
* the `Active packs:` line in the AGENT.md right beside it.
|
|
102
|
+
*/
|
|
103
|
+
const OWNED_SRC_DIRS = ['laws', 'agents', 'skills', 'ontology', 'doctrines'];
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The constitution dirs a LEAN emit re-ships in full text — see `shipLeanLaws`.
|
|
107
|
+
*
|
|
108
|
+
* Not all of `OWNED_SRC_DIRS`: `agents` and `skills` are already written as whole files by
|
|
109
|
+
* the native layer, and only the three constitution dirs are inlined into AGENT.md in a
|
|
110
|
+
* form (terse, or pack-filtered) that loses text the reader may still need.
|
|
111
|
+
*/
|
|
112
|
+
const LEAN_FULL_TEXT_DIRS = ['laws', 'ontology', 'doctrines'];
|
|
92
113
|
|
|
93
114
|
/**
|
|
94
115
|
* `_build_core.CAPABILITY_LINK_RE` — the DEFAULT only.
|
|
@@ -824,7 +845,8 @@ export function emitOpencodeRender(cfg, job) {
|
|
|
824
845
|
// (`_rel_under` is the Python side by design), and only the BASENAME is consumed
|
|
825
846
|
// downstream: `opencode.json`, or the `.jsonc` sibling when that is what is on disk.
|
|
826
847
|
phaseLog('WIRE');
|
|
827
|
-
const cfgName = path.basename(mergeOpencodeJson(path.join(root, 'opencode.json'), agentPath
|
|
848
|
+
const cfgName = path.basename(mergeOpencodeJson(path.join(root, 'opencode.json'), agentPath,
|
|
849
|
+
cfg.doctrines));
|
|
828
850
|
|
|
829
851
|
return {
|
|
830
852
|
owned,
|
|
@@ -929,16 +951,27 @@ function globalNotebook(cfgDir, items, legacy, srcRoot) {
|
|
|
929
951
|
* output besides the files is what it APPENDS to `owned` — which travels back in the
|
|
930
952
|
* payload for the manifest and the prune. It reads nothing any wiring stage writes, so
|
|
931
953
|
* its new position is not merely legal, it has no dependency on the old one.
|
|
954
|
+
*
|
|
955
|
+
* NO LONGER LEAN-ONLY, and the name is kept because the lean case is still the whole reason
|
|
956
|
+
* it exists. `doctrines` ships at BOTH footprints, because the two footprints lose different
|
|
957
|
+
* things: lean loses text (every rule is truncated to its first sentence, so the full text has
|
|
958
|
+
* to be on disk), while FULL loses whole packs (only the active ones are inlined, so the
|
|
959
|
+
* inactive ones have to be on disk). AGENT.md tells the agent it may read a rule in a pack
|
|
960
|
+
* that is not active, and `src/doctrines/README.md` rests cross-pack citations on exactly
|
|
961
|
+
* that — a claim the emit has to make true, not merely the `files` bundle.
|
|
932
962
|
*/
|
|
933
|
-
function shipLeanLaws(items, theme, cfgDir, owned) {
|
|
934
|
-
|
|
963
|
+
function shipLeanLaws(items, theme, cfgDir, owned, footprint) {
|
|
964
|
+
// Themed names, resolved through the same token table `build`'s wipe resolves, so the dir
|
|
965
|
+
// a lean emit re-ships and the dir it prunes can never be two different spellings.
|
|
966
|
+
const wanted = footprint === 'lean' ? LEAN_FULL_TEXT_DIRS : ['doctrines'];
|
|
967
|
+
const dirs = new Set(wanted.map((d) => theme[SRC_DIR_TOKENS[d]] ?? d));
|
|
935
968
|
for (const { rel, text } of items) {
|
|
936
969
|
const parts = rel.split('/');
|
|
937
970
|
// `parts.length` mirrors Python's `parts and parts[0]`, where `Path("").parts` really
|
|
938
971
|
// is empty. `String.split` never returns an empty array, so this half can never be
|
|
939
972
|
// false here — kept for the mirror, and its mutation stays green because there is
|
|
940
973
|
// nothing to detect, not because the gate cannot see it.
|
|
941
|
-
if (text !== null && parts.length && parts[0]
|
|
974
|
+
if (text !== null && parts.length && dirs.has(parts[0])) {
|
|
942
975
|
const dest = path.join(cfgDir, ...parts);
|
|
943
976
|
mkdirSync(path.dirname(dest), { recursive: true });
|
|
944
977
|
writeText(dest, text);
|
|
@@ -1039,7 +1072,7 @@ export function emitOpencodeGlobalRender(cfg, job) {
|
|
|
1039
1072
|
writeVersion(cfg, cfgDir);
|
|
1040
1073
|
owned.push(VERSION_MARKER);
|
|
1041
1074
|
|
|
1042
|
-
|
|
1075
|
+
shipLeanLaws(items, theme, cfgDir, owned, footprint);
|
|
1043
1076
|
|
|
1044
1077
|
// WIRE — the one file of this emit the user co-owns, and the last render is now behind
|
|
1045
1078
|
// us. Inlined the way `emitOpencodeRender`'s is (one call, the same merge, the same
|
|
@@ -1047,7 +1080,7 @@ export function emitOpencodeGlobalRender(cfg, job) {
|
|
|
1047
1080
|
// that split is what `tests/test_emit_phase_order.py` walks, and it walks the Python.
|
|
1048
1081
|
phaseLog('WIRE');
|
|
1049
1082
|
const cfgName = path.basename(mergeOpencodeJson(path.join(cfgDir, 'opencode.json'),
|
|
1050
|
-
agentPath));
|
|
1083
|
+
agentPath, cfg.doctrines));
|
|
1051
1084
|
|
|
1052
1085
|
return {
|
|
1053
1086
|
owned,
|
|
@@ -1170,7 +1203,7 @@ export function emitClaudeRender(cfg, job) {
|
|
|
1170
1203
|
writeVersion(cfg, cfgDir);
|
|
1171
1204
|
owned.push(VERSION_MARKER);
|
|
1172
1205
|
|
|
1173
|
-
|
|
1206
|
+
shipLeanLaws(items, theme, cfgDir, owned, footprint);
|
|
1174
1207
|
|
|
1175
1208
|
// The last render of the emit, and the only one whose product is not written here.
|
|
1176
1209
|
// Bob at GLOBAL scope gets no managed block at all, so nothing is rendered for it —
|
|
@@ -1183,7 +1216,7 @@ export function emitClaudeRender(cfg, job) {
|
|
|
1183
1216
|
|
|
1184
1217
|
// WIRE — see `claudeWire` below. One child per emit, so the two halves are two
|
|
1185
1218
|
// functions rather than two spawns; the seam between them is a call, not a process.
|
|
1186
|
-
const managed = claudeWire(job, claudeMdText, agentText !== null);
|
|
1219
|
+
const managed = claudeWire(job, claudeMdText, agentText !== null, cfg.doctrines);
|
|
1187
1220
|
|
|
1188
1221
|
return {
|
|
1189
1222
|
owned,
|
|
@@ -1218,7 +1251,7 @@ export function emitClaudeRender(cfg, job) {
|
|
|
1218
1251
|
* not needed for it: Python tests `os.environ.get(...)` for truthiness and an env var is
|
|
1219
1252
|
* always a string, so `''` is the only falsy value either language sees.
|
|
1220
1253
|
*/
|
|
1221
|
-
function claudeWire(job, claudeMdText, hasAgentText) {
|
|
1254
|
+
function claudeWire(job, claudeMdText, hasAgentText, doctrines = null) {
|
|
1222
1255
|
phaseLog('WIRE');
|
|
1223
1256
|
const { cfgDir, claudeMd, scope, host, oldManaged, preambleExclude, hookOpts } = job;
|
|
1224
1257
|
const old = oldManaged && typeof oldManaged === 'object' && !Array.isArray(oldManaged)
|
|
@@ -1274,8 +1307,12 @@ function claudeWire(job, claudeMdText, hasAgentText) {
|
|
|
1274
1307
|
// The merge prunes recorded groups that are no longer canonical and returns the
|
|
1275
1308
|
// COMPLETE current claim set — store it as-is; unioning with prior would resurrect the
|
|
1276
1309
|
// stale claims.
|
|
1310
|
+
// `doctrines` decides whether the git-gate group is part of the canonical claim set. It
|
|
1311
|
+
// travels from `cfg` rather than being re-read off the deployment: this is the emit that
|
|
1312
|
+
// DECIDES the install's packs, so the marker on disk is still the previous build's.
|
|
1277
1313
|
const [, managedHooks] = mergeClaudeSettings(
|
|
1278
1314
|
settingsPath, scope, oldSf === settingsName ? get(old, 'settings_hooks') : null, hookOpts,
|
|
1315
|
+
doctrines,
|
|
1279
1316
|
);
|
|
1280
1317
|
managed.settings_hooks = managedHooks;
|
|
1281
1318
|
|