geneseed 4.9.4 → 4.14.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 +106 -0
- package/README.md +16 -2
- package/SHIPPED.md +1 -1
- package/docs/README.md +1 -1
- package/docs/concepts/collaboration.md +1 -1
- package/docs/concepts/enforcement-table.md +1 -1
- package/docs/concepts/foreman-mode.md +1 -1
- package/docs/concepts/kits.md +91 -0
- package/docs/concepts/loop-branch.md +1 -1
- package/docs/concepts/loop-bricks.md +1 -1
- package/docs/concepts/loop-contracts.md +1 -1
- package/docs/concepts/loop-gates.md +1 -1
- package/docs/concepts/loop-notes.md +1 -1
- package/docs/concepts/loop-risk.md +1 -1
- package/docs/concepts/loop-templates.md +1 -1
- package/docs/concepts/loops.md +1 -1
- package/docs/concepts/lsp.md +1 -1
- package/docs/concepts/skills.md +1 -1
- package/docs/extending.md +107 -4
- package/docs/guides/install-scripted.md +1 -1
- package/docs/guides/run-anywhere.md +2 -2
- package/docs/guides/upgrade.md +2 -2
- package/docs/reference/glossary.md +1 -0
- package/docs/reference/troubleshoot-install.md +1 -1
- package/docs/web-ui.md +9 -0
- package/harness.config.json +1 -1
- package/js/build/args.mjs +27 -6
- package/js/build/driver.mjs +29 -8
- package/js/build/emit-claude.mjs +3 -1
- package/js/build/generate.mjs +10 -5
- package/js/build/render.mjs +94 -11
- package/js/build/source.mjs +117 -4
- package/js/cli-table.json +15 -0
- package/js/hosts/installs.mjs +35 -2
- package/js/hosts/link.mjs +88 -1
- package/js/hosts/native.mjs +14 -4
- package/js/inspect/checks-authoring.mjs +6 -3
- package/js/inspect/checks-build.mjs +15 -3
- package/js/inspect/checks-counts.mjs +153 -6
- package/js/inspect/checks-repo.mjs +95 -20
- package/js/inspect/cost.mjs +1 -1
- package/js/inspect/diff.mjs +56 -8
- package/js/inspect/inventory.mjs +2 -0
- package/js/inspect/status.mjs +44 -3
- package/js/inspect/validate.mjs +3 -3
- package/js/lib/prompt.mjs +19 -2
- package/js/maintain/setup.mjs +111 -23
- package/js/maintain/update.mjs +3 -3
- package/js/web/actions.mjs +38 -6
- package/js/web/api.mjs +76 -17
- package/js/web/catalog.mjs +17 -2
- package/js/web/handler.mjs +1 -0
- package/js/web/routes.mjs +2 -1
- package/js/web/server.mjs +11 -5
- package/js/web/state.mjs +38 -3
- package/package.json +1 -1
- package/registry.json +91 -0
- package/src/AGENT.md.tmpl +7 -7
- package/src/agents/_self-improvement.md +11 -0
- package/src/agents/_template.md +1 -8
- package/src/agents/advocate.md +1 -8
- package/src/agents/architect.md +1 -8
- package/src/agents/developer.md +1 -8
- package/src/agents/devops-engineer.md +37 -0
- package/src/agents/docker-expert.md +37 -0
- package/src/agents/docs.md +1 -8
- package/src/agents/empiricist.md +1 -8
- package/src/agents/explorer.md +1 -8
- package/src/agents/framer.md +1 -8
- package/src/agents/historian.md +1 -8
- package/src/agents/kubernetes-specialist.md +37 -0
- package/src/agents/memory-curator.md +1 -8
- package/src/agents/operator.md +1 -8
- package/src/agents/performance.md +1 -8
- package/src/agents/pragmatist.md +1 -8
- package/src/agents/react-engineer.md +38 -0
- package/src/agents/react-reviewer.md +42 -0
- package/src/agents/researcher.md +5 -10
- package/src/agents/reviewer.md +3 -8
- package/src/agents/security.md +1 -8
- package/src/agents/skeptic.md +1 -8
- package/src/agents/spring-boot-engineer.md +40 -0
- package/src/agents/spring-reviewer.md +41 -0
- package/src/agents/spring-security.md +42 -0
- package/src/agents/steward.md +1 -8
- package/src/agents/tester.md +1 -8
- package/src/agents/user-advocate.md +1 -8
- package/src/agents/visionary.md +1 -8
- package/src/kits/react/rules.md +13 -0
- package/src/kits/react.json +18 -0
- package/src/kits/spring-boot/rules.md +10 -0
- package/src/kits/spring-boot.json +23 -0
- package/src/licenses/everything-claude-code.txt +21 -0
- package/src/licenses/mattpocock-skills.txt +21 -0
- package/src/licenses/piomin-spring-boot.txt +190 -0
- package/src/licenses/superpowers.txt +21 -0
- package/src/licenses/vibe-wise.txt +21 -0
- package/src/skills/_self-improvement.md +22 -5
- package/src/skills/brainstorm/builder-check.md +2 -0
- package/src/skills/brainstorm.md +11 -2
- package/src/skills/codebase-design.md +10 -2
- package/src/skills/daydream/VENDOR.md +7 -7
- package/src/skills/debug.md +6 -2
- package/src/skills/develop.md +6 -2
- package/src/skills/geneseed-code-review.md +6 -2
- package/src/skills/git-history.md +2 -0
- package/src/skills/handoff.md +3 -1
- package/src/skills/learn-mode.md +3 -2
- package/src/skills/parallel-agents.md +5 -1
- package/src/skills/pipeline.md +2 -0
- package/src/skills/plan.md +9 -2
- package/src/skills/prototype.md +6 -2
- package/src/skills/react/a11y.md +33 -0
- package/src/skills/react/vite.md +35 -0
- package/src/skills/react/webpack.md +40 -0
- package/src/skills/react-testing.md +68 -0
- package/src/skills/react-view-transitions/AGENTS.md +646 -221
- package/src/skills/react-view-transitions/SKILL.md +28 -16
- package/src/skills/react-view-transitions/VENDOR.md +10 -8
- package/src/skills/react-view-transitions/references/css-recipes.md +72 -2
- package/src/skills/react-view-transitions/references/implementation.md +14 -26
- package/src/skills/react-view-transitions/references/nextjs.md +94 -37
- package/src/skills/react-view-transitions/references/patterns.md +60 -48
- package/src/skills/react-view-transitions/references/troubleshooting.md +47 -0
- package/src/skills/react.md +63 -0
- package/src/skills/research.md +6 -2
- package/src/skills/review-response.md +2 -0
- package/src/skills/rule.md +6 -2
- package/src/skills/ship.md +8 -4
- package/src/skills/skill-forge.md +11 -5
- package/src/skills/spring-boot/cloud.md +103 -0
- package/src/skills/spring-boot/java-modern.md +71 -0
- package/src/skills/spring-boot/logging.md +89 -0
- package/src/skills/spring-boot/security.md +112 -0
- package/src/skills/spring-boot/web.md +112 -0
- package/src/skills/spring-boot.md +57 -0
- package/src/skills/spring-data-jpa/patterns.md +68 -0
- package/src/skills/spring-data-jpa.md +81 -0
- package/src/skills/spring-testing.md +93 -0
- package/src/skills/wayfinder.md +8 -2
- package/src/skills/wiki.md +18 -5
- package/src/upstreams.json +66 -0
- package/themes/_TEMPLATE.json +13 -0
- package/themes/biker.json +13 -0
- package/themes/commentator.json +13 -0
- package/themes/cyberpunk.json +13 -0
- package/themes/gamer.json +13 -0
- package/themes/imperial.json +13 -0
- package/themes/joker.json +13 -0
- package/themes/marvin.json +13 -0
- package/themes/mean.json +13 -0
- package/themes/military.json +13 -0
- package/themes/neutral.json +13 -0
- package/themes/pirate.json +13 -0
- package/themes/sports.json +13 -0
- package/themes/verstappen.json +13 -0
- package/themes/wizard.json +13 -0
- package/web/dist/assets/Activity-44rbWUQI.js +1 -0
- package/web/dist/assets/ActivityDetail-Dk9k893a.js +1 -0
- package/web/dist/assets/Dashboard-DCoUN60W.js +2 -0
- package/web/dist/assets/{Docs-tDW7i7mo.js → Docs-C_je2lRG.js} +3 -3
- package/web/dist/assets/FilterInput-CZREvrjX.js +1 -0
- package/web/dist/assets/GreenhouseView-BXAOIznW.js +2 -0
- package/web/dist/assets/Installs-DAXF403D.js +3 -0
- package/web/dist/assets/JournalView-DZ184wKB.js +1 -0
- package/web/dist/assets/KitApply-pjhvGha0.js +1 -0
- package/web/dist/assets/Kits-BCCKjSIY.js +1 -0
- package/web/dist/assets/{Laws-BA3CzUf5.js → Laws-BgBUpRkE.js} +1 -1
- package/web/dist/assets/Library-CW0Zl6RF.js +1 -0
- package/web/dist/assets/Loops-BvFOzjEw.js +2 -0
- package/web/dist/assets/{Markdown-iHvBrpRN.js → Markdown-zaXXXHz9.js} +13 -13
- package/web/dist/assets/OperatorHudView-Bs4unPNi.js +2 -0
- package/web/dist/assets/Personal-C23XaT-a.js +1 -0
- package/web/dist/assets/Tabs-b2XFu4OS.js +1 -0
- package/web/dist/assets/activity-Jw8XcPiP.js +1 -0
- package/web/dist/assets/api-BfpZmUMI.js +1 -0
- package/web/dist/assets/{drawable-CyLe1gtW.js → drawable-BERkKhx_.js} +1 -1
- package/web/dist/assets/index-C9R_bRw_.js +2 -0
- package/web/dist/assets/{index-BCJz4_8C.css → index-jYlWihN-.css} +1 -1
- package/web/dist/assets/{react-BOF6yL40.js → react-DlYQp10C.js} +1 -1
- package/web/dist/assets/rolldown-runtime-hePW80VL.js +1 -0
- package/web/dist/assets/sections-ChwaobfW.js +1 -0
- package/web/dist/assets/usePoll-bDFN-Hr6.js +1 -0
- package/web/dist/index.html +6 -5
- package/web/dist/assets/Activity-C8sZVIUU.js +0 -1
- package/web/dist/assets/ActivityDetail-Cc6vWUS4.js +0 -1
- package/web/dist/assets/Dashboard-C5ATa3ow.js +0 -2
- package/web/dist/assets/FilterInput-zLFXoSt6.js +0 -1
- package/web/dist/assets/GreenhouseView-zk2Uzc3A.js +0 -2
- package/web/dist/assets/Installs-CB_rvbBP.js +0 -3
- package/web/dist/assets/JournalView-Bfj27XHC.js +0 -1
- package/web/dist/assets/Library-BWwiy9Vb.js +0 -1
- package/web/dist/assets/Loops-DaPk-ckL.js +0 -2
- package/web/dist/assets/OperatorHudView-CyiGhiHk.js +0 -2
- package/web/dist/assets/Personal-Bwd27CmT.js +0 -1
- package/web/dist/assets/Tabs-BOD7Kaz1.js +0 -1
- package/web/dist/assets/activity-DvPxoS4Y.js +0 -1
- package/web/dist/assets/api-CiXlkbe0.js +0 -1
- package/web/dist/assets/index-DQXG_mqo.js +0 -2
- package/web/dist/assets/sections-BET3c1WT.js +0 -1
- package/web/dist/assets/usePoll-NSTzLOF4.js +0 -1
package/js/hosts/installs.mjs
CHANGED
|
@@ -36,7 +36,7 @@ import { existsSync, readdirSync } from 'node:fs';
|
|
|
36
36
|
import path from 'node:path';
|
|
37
37
|
|
|
38
38
|
import {
|
|
39
|
-
CONFIG, GATE_SWITCHES, PACK_ORDER, THEMES, discoverNames, resolveSkillNames,
|
|
39
|
+
CONFIG, GATE_SWITCHES, PACK_ORDER, RETIRED_KIT_IDS, THEMES, discoverNames, loadKits, resolveSkillNames,
|
|
40
40
|
} from '../build/source.mjs';
|
|
41
41
|
import {
|
|
42
42
|
CLAUDE_STYLE, DISABLED_STASH, GLOBAL_MANIFEST, HOSTS, isHostGlobalDir, liveRecordedHooks,
|
|
@@ -585,6 +585,33 @@ export function excludedSkillsOfDir(d, host = null, notify = null) {
|
|
|
585
585
|
}, [], host);
|
|
586
586
|
}
|
|
587
587
|
|
|
588
|
+
/** `Active kits: spring-boot` — written only when a kit is on; the skills line's shape. */
|
|
589
|
+
const KITS_RE = /^Active kits:[ \t]*(.+?)[ \t]*$/m;
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* The kits a DEPLOYED install has on, read back so `rebuild-all`, `upgrade`, `migrate`, `diff`, the wizard
|
|
593
|
+
* and the console re-render them (spec A3). A missing line is `[]`: no kits, the old install's
|
|
594
|
+
* state. Unlike `excludedSkillsOfDir`, a name this checkout does not ship drops ALONE, with a
|
|
595
|
+
* notice (spec R2): a retired kit must never reach `--kits`, which refuses it and would fail the
|
|
596
|
+
* whole `rebuild-all`, and the kits beside it are still the owner's choice. Silent without `notify`.
|
|
597
|
+
*/
|
|
598
|
+
export function kitsOfDir(d, host = null, notify = null) {
|
|
599
|
+
return firstCarrier(d, (carrierPath) => {
|
|
600
|
+
const text = readMaybe(carrierPath);
|
|
601
|
+
if (text === null) return undefined;
|
|
602
|
+
const m = KITS_RE.exec(text);
|
|
603
|
+
if (!m) return undefined;
|
|
604
|
+
const known = loadKits();
|
|
605
|
+
const names = [...new Set(m[1].split(',').map((s) => s.trim()).filter(Boolean))].sort();
|
|
606
|
+
for (const k of names.filter((n) => !known.has(n))) {
|
|
607
|
+
notify?.(Object.hasOwn(RETIRED_KIT_IDS, k)
|
|
608
|
+
? `kit '${k}' was retired (${RETIRED_KIT_IDS[k]}) — dropped`
|
|
609
|
+
: `kit '${k}' is not shipped by this Geneseed — dropped`);
|
|
610
|
+
}
|
|
611
|
+
return names.filter((n) => known.has(n));
|
|
612
|
+
}, [], host);
|
|
613
|
+
}
|
|
614
|
+
|
|
588
615
|
/**
|
|
589
616
|
* Whether a DEPLOYED install keeps one law's hook gate armed — `true` (armed) or `false` (its
|
|
590
617
|
* `<marker>: off` line, written by a build with that gate's `--no-*-gate` flag; `GATE_SWITCHES`).
|
|
@@ -861,7 +888,7 @@ export function installsReferencingShim(shimPath = hookShimPath(), targets = ins
|
|
|
861
888
|
export function installedDefaults() {
|
|
862
889
|
const found = {
|
|
863
890
|
theme: null, posture: null, mode: null, emit: null, footprint: null, doctrines: null,
|
|
864
|
-
excludeRules: null, excludeSkills: null,
|
|
891
|
+
excludeRules: null, excludeSkills: null, kits: null,
|
|
865
892
|
};
|
|
866
893
|
const candidates = [];
|
|
867
894
|
for (const { host, configDir } of HOSTS) {
|
|
@@ -904,6 +931,12 @@ export function installedDefaults() {
|
|
|
904
931
|
// indistinguishable here, which is exactly what the marker itself already says.
|
|
905
932
|
if (!found.excludeRules?.length) found.excludeRules = excludedRulesOfDir(base);
|
|
906
933
|
if (!found.excludeSkills?.length) found.excludeSkills = excludedSkillsOfDir(base);
|
|
934
|
+
// Kits walk the same two-state way: no line and `none` both read `[]`.
|
|
935
|
+
// A broken manifest must not crash the wizard here: the build that follows still fails loudly
|
|
936
|
+
// in `loadKits`. Not guarded inside `kitsOfDir`, which rebuild-all must keep failing on.
|
|
937
|
+
if (!found.kits?.length) {
|
|
938
|
+
try { found.kits = kitsOfDir(base); } catch { found.kits = []; }
|
|
939
|
+
}
|
|
907
940
|
}
|
|
908
941
|
return found;
|
|
909
942
|
}
|
package/js/hosts/link.mjs
CHANGED
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
import {
|
|
75
75
|
chmodSync, existsSync, lstatSync, mkdirSync, readlinkSync, rmSync,
|
|
76
76
|
} from 'node:fs';
|
|
77
|
+
import { isatty } from 'node:tty';
|
|
77
78
|
import { spawnSync } from 'node:child_process';
|
|
78
79
|
import path from 'node:path';
|
|
79
80
|
import os from 'node:os';
|
|
@@ -81,12 +82,84 @@ import os from 'node:os';
|
|
|
81
82
|
import { ROOT } from '../build/source.mjs';
|
|
82
83
|
import { readMaybe } from './installs.mjs';
|
|
83
84
|
import { printOut, printErr, writeText } from '../lib/fs.mjs';
|
|
85
|
+
import { confirm } from '../lib/prompt.mjs';
|
|
84
86
|
import { toPlatformPath, which } from '../lib/paths.mjs';
|
|
85
87
|
|
|
86
88
|
const IS_WIN = process.platform === 'win32';
|
|
87
89
|
/** What `cmdUnlink` recognises a written Unix launcher by — see `isOurLauncher`. */
|
|
88
90
|
const SHIM_MARKER = 'GENESEED_LINK_SHIM';
|
|
89
91
|
|
|
92
|
+
/** The lines that open and close the PATH block `link` writes into a shell startup file. */
|
|
93
|
+
const RC_BEGIN = '# >>> geneseed >>> (added by `geneseed link`; `geneseed unlink` removes it)';
|
|
94
|
+
const RC_END = '# <<< geneseed <<<';
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The startup file to put the bin dir on PATH in, for this login shell — or null when it is
|
|
98
|
+
* not one we know how to edit (fish, csh, none), where `link` prints the line instead.
|
|
99
|
+
*
|
|
100
|
+
* zsh reads `$ZDOTDIR/.zshrc` (default `~`). bash reads `~/.bashrc` on Linux, but macOS
|
|
101
|
+
* Terminal opens LOGIN shells, which read `~/.bash_profile` and not `~/.bashrc`. Pure, for the
|
|
102
|
+
* test: nothing here may touch the real user's files.
|
|
103
|
+
*/
|
|
104
|
+
export function shellRcFile({
|
|
105
|
+
shell = process.env.SHELL || '', home = os.homedir(), zdotdir = process.env.ZDOTDIR,
|
|
106
|
+
platform = process.platform,
|
|
107
|
+
} = {}) {
|
|
108
|
+
const name = path.basename(shell);
|
|
109
|
+
if (name === 'zsh') return path.join(zdotdir || home, '.zshrc');
|
|
110
|
+
if (name === 'bash') return path.join(home, platform === 'darwin' ? '.bash_profile' : '.bashrc');
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* `text` with the PATH block for `dir` appended, or unchanged when a block is already there.
|
|
116
|
+
*
|
|
117
|
+
* The dir goes inside double quotes with `\`, `"`, `$` and backtick escaped, so a home path
|
|
118
|
+
* holding any of them stays one literal word to the shell rather than becoming code.
|
|
119
|
+
*/
|
|
120
|
+
export function withPathBlock(text, dir) {
|
|
121
|
+
if (text.includes(RC_BEGIN)) return text;
|
|
122
|
+
const quoted = dir.replace(/[\\"$`]/g, (c) => `\\${c}`);
|
|
123
|
+
const sep = text === '' || text.endsWith('\n') ? '' : '\n';
|
|
124
|
+
return `${text}${sep}${RC_BEGIN}\nexport PATH="${quoted}:$PATH"\n${RC_END}\n`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** `text` without the PATH block `withPathBlock` wrote; unchanged when there is none. */
|
|
128
|
+
export function withoutPathBlock(text) {
|
|
129
|
+
const start = text.indexOf(RC_BEGIN);
|
|
130
|
+
if (start < 0) return text;
|
|
131
|
+
const end = text.indexOf(RC_END, start);
|
|
132
|
+
if (end < 0) return text;
|
|
133
|
+
let stop = end + RC_END.length;
|
|
134
|
+
if (text[stop] === '\n') stop += 1;
|
|
135
|
+
return text.slice(0, start) + text.slice(stop);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Put `dir` on PATH for future shells, ASKING first — macOS zsh does not search
|
|
140
|
+
* `~/.local/bin`, so a linked `geneseed` was not found (reported 2026-10). Only in a terminal:
|
|
141
|
+
* the web console's button and every test run without one, and get the printed line instead,
|
|
142
|
+
* so no gate can edit the developer's own startup file. Returns whether it wrote.
|
|
143
|
+
*/
|
|
144
|
+
function offerShellPath(dir) {
|
|
145
|
+
const rc = shellRcFile();
|
|
146
|
+
if (!rc || !isatty(0)) return false;
|
|
147
|
+
const text = readMaybe(rc) ?? '';
|
|
148
|
+
if (text.includes(RC_BEGIN)) {
|
|
149
|
+
printOut(`geneseed: ${rc} already adds it — open a NEW terminal, then run 'geneseed'.\n`);
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
if (!confirm(`geneseed: add '${dir}' to your PATH in ${rc}?`, true)) return false;
|
|
153
|
+
try {
|
|
154
|
+
writeText(rc, withPathBlock(text, dir));
|
|
155
|
+
} catch (e) {
|
|
156
|
+
printErr(`geneseed: could not write ${rc} (${asOsError(e)})\n`);
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
printOut(`geneseed: added it to ${rc} — open a NEW terminal, then run 'geneseed'.\n`);
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
90
163
|
/** `_harness_lifecycle._win_bin_dir`. */
|
|
91
164
|
function winBinDir() {
|
|
92
165
|
// `os.environ.get(...) or str(Path.home())` — an EMPTY LOCALAPPDATA falls back too,
|
|
@@ -267,7 +340,7 @@ export function cmdLink(args) {
|
|
|
267
340
|
printOut(`geneseed: linked ${dest} -> ${entry}\n`);
|
|
268
341
|
if ((process.env.PATH || '').split(path.delimiter).includes(targetDir)) {
|
|
269
342
|
printOut(`geneseed: '${targetDir}' is on PATH — run 'geneseed' from anywhere.\n`);
|
|
270
|
-
} else {
|
|
343
|
+
} else if (!offerShellPath(targetDir)) {
|
|
271
344
|
printOut(`geneseed: NOTE '${targetDir}' is not on your PATH. Add it, e.g.:\n`);
|
|
272
345
|
printOut(` echo 'export PATH="${targetDir}:$PATH"' >> ~/.zshrc # or ~/.bashrc\n`);
|
|
273
346
|
}
|
|
@@ -311,6 +384,20 @@ export function cmdUnlink() {
|
|
|
311
384
|
} catch { /* `except OSError: pass` */ }
|
|
312
385
|
}
|
|
313
386
|
}
|
|
387
|
+
// The PATH block `link` may have written, from each startup file it could have chosen.
|
|
388
|
+
const home = os.homedir();
|
|
389
|
+
const rcs = new Set([shellRcFile({ shell: 'zsh' }), path.join(home, '.bashrc'),
|
|
390
|
+
path.join(home, '.bash_profile')]);
|
|
391
|
+
for (const rc of rcs) {
|
|
392
|
+
const text = readMaybe(rc);
|
|
393
|
+
if (text === null || withoutPathBlock(text) === text) continue;
|
|
394
|
+
try {
|
|
395
|
+
writeText(rc, withoutPathBlock(text));
|
|
396
|
+
printOut(`geneseed: removed the PATH line from ${rc}\n`);
|
|
397
|
+
} catch (e) {
|
|
398
|
+
printErr(`geneseed: could not edit ${rc} (${asOsError(e)})\n`);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
314
401
|
if (!removed) printOut('geneseed: no linked launcher found on PATH\n');
|
|
315
402
|
return 0;
|
|
316
403
|
}
|
package/js/hosts/native.mjs
CHANGED
|
@@ -579,7 +579,7 @@ export function writeNativeLayer(items, agentsDir, skillsDir, overrides = null,
|
|
|
579
579
|
let nAgents = 0;
|
|
580
580
|
let nSkills = 0;
|
|
581
581
|
const written = [];
|
|
582
|
-
const sideFiles = []; // [stem, dest, text], written after the loop
|
|
582
|
+
const sideFiles = []; // [stem, dest, text, source], written after the loop
|
|
583
583
|
const claimedSkills = new Set(); // stems whose own SKILL.md claim held
|
|
584
584
|
const userOnlySkills = []; // claimed stems carrying the user-only marker
|
|
585
585
|
|
|
@@ -589,7 +589,13 @@ export function writeNativeLayer(items, agentsDir, skillsDir, overrides = null,
|
|
|
589
589
|
written.push(dest);
|
|
590
590
|
};
|
|
591
591
|
|
|
592
|
-
for (const { text, src: source } of items) {
|
|
592
|
+
for (const { text, src: source, rel, licenceOf } of items) {
|
|
593
|
+
// A credited skill's upstream licence: copied verbatim beside its SKILL.md, held like a side
|
|
594
|
+
// file until that claim holds. Routed by `licenceOf`, since `source` is the upstream's file.
|
|
595
|
+
if (licenceOf) {
|
|
596
|
+
sideFiles.push([licenceOf, path.join(skillsDir, licenceOf, path.posix.basename(rel)), null, source]);
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
593
599
|
const sparts = relPosix(src, source).split('/');
|
|
594
600
|
|
|
595
601
|
// Vendored third-party skill folders ride along verbatim, preserving their own
|
|
@@ -706,8 +712,12 @@ export function writeNativeLayer(items, agentsDir, skillsDir, overrides = null,
|
|
|
706
712
|
if (kind === 'agent') nAgents += 1;
|
|
707
713
|
else nSkills += 1;
|
|
708
714
|
}
|
|
709
|
-
for (const [stem, dest, text] of sideFiles) {
|
|
710
|
-
if (claimedSkills.has(stem)
|
|
715
|
+
for (const [stem, dest, text, source] of sideFiles) {
|
|
716
|
+
if (!claimedSkills.has(stem) || !claim(dest)) continue;
|
|
717
|
+
if (text !== null) { write(dest, stripSkillBodyLinks(text)); continue; }
|
|
718
|
+
mkdirSync(path.dirname(dest), { recursive: true });
|
|
719
|
+
copyFile(source, dest);
|
|
720
|
+
written.push(dest);
|
|
711
721
|
}
|
|
712
722
|
return { nAgents, nSkills, written, userOnlySkills };
|
|
713
723
|
}
|
|
@@ -21,11 +21,11 @@ import { blankCodeSpans } from '../lib/text.mjs';
|
|
|
21
21
|
import { which } from '../lib/paths.mjs';
|
|
22
22
|
import { NO_WINDOW } from '../lib/proc.mjs';
|
|
23
23
|
import {
|
|
24
|
-
aliasProblems, folderSkillProblems, registryProblems, secretProblems, sideFileProblems,
|
|
24
|
+
aliasProblems, creditProblems, folderSkillProblems, registryProblems, secretProblems, sideFileProblems,
|
|
25
25
|
vendorPinProblems,
|
|
26
26
|
} from './checks-repo.mjs';
|
|
27
27
|
import { constitutionProblems, leanBlockProblems } from './checks-constitution.mjs';
|
|
28
|
-
import { countTableProblems } from './checks-counts.mjs';
|
|
28
|
+
import { countTableProblems, kitProblems } from './checks-counts.mjs';
|
|
29
29
|
import { catalogProblems } from '../loop/catalog.mjs';
|
|
30
30
|
import { globSorted, isDir, isFile, rglob } from './scan.mjs';
|
|
31
31
|
import { spawnSync } from 'node:child_process';
|
|
@@ -55,6 +55,7 @@ const ALLOWED_COMMENT_RES = [
|
|
|
55
55
|
// js/web/docs.mjs HARNESS_OPEN_RE: `harness:` then one or more of its four ids, comma-separated.
|
|
56
56
|
new RegExp(`^harness:\\s*(?:${HARNESS_IDS})(?:\\s*,\\s*(?:${HARNESS_IDS}))*$`),
|
|
57
57
|
/^bash:\s*allow$/, // js/hosts/native.mjs BASH_MARKER
|
|
58
|
+
/^banned: ok$/, // checks-counts.mjs kitProblems: a kit line that names a banned string
|
|
58
59
|
];
|
|
59
60
|
|
|
60
61
|
/**
|
|
@@ -114,7 +115,7 @@ export function authorNoteProblems(src = SRC) {
|
|
|
114
115
|
export {
|
|
115
116
|
HOOK_PINNED, constitutionProblems, doctrineMetaProblems, lawMetaProblems, leanBlockProblems,
|
|
116
117
|
} from './checks-constitution.mjs';
|
|
117
|
-
export { countTableProblems, proseMirrorProblems } from './checks-counts.mjs';
|
|
118
|
+
export { countTableProblems, kitProblems, proseMirrorProblems } from './checks-counts.mjs';
|
|
118
119
|
|
|
119
120
|
/**
|
|
120
121
|
* The shipped loop catalogue parses and every shipped template passes the engine's own graph
|
|
@@ -192,10 +193,12 @@ export function authoringProblems() {
|
|
|
192
193
|
problems.push(...vendorPinProblems());
|
|
193
194
|
problems.push(...folderSkillProblems());
|
|
194
195
|
problems.push(...aliasProblems());
|
|
196
|
+
problems.push(...creditProblems());
|
|
195
197
|
problems.push(...sideFileProblems());
|
|
196
198
|
problems.push(...constitutionProblems());
|
|
197
199
|
problems.push(...leanBlockProblems());
|
|
198
200
|
problems.push(...countTableProblems());
|
|
201
|
+
problems.push(...kitProblems());
|
|
199
202
|
problems.push(...authorNoteProblems());
|
|
200
203
|
return problems;
|
|
201
204
|
}
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { stripCapabilityLinks } from '../build/emit-common.mjs';
|
|
11
11
|
import { STRUCTURE, renderAll } from '../build/render.mjs';
|
|
12
|
-
import { CONFIG, makeCfg } from '../build/source.mjs';
|
|
13
|
-
import { hostCatalogsNatively, resolvePath } from '../hosts/hosts.mjs';
|
|
12
|
+
import { CONFIG, CREDIT_RE, makeCfg } from '../build/source.mjs';
|
|
13
|
+
import { CLAUDE_STYLE, hostCatalogsNatively, resolvePath } from '../hosts/hosts.mjs';
|
|
14
14
|
import { EMIT_HOST_SCOPE, footprintOfDir, themeFiles } from '../hosts/installs.mjs';
|
|
15
15
|
import { isVendoredPath } from '../hosts/native.mjs';
|
|
16
16
|
import { PALETTE_ROLES, colorThemeFiles } from '../hosts/opencode.mjs';
|
|
@@ -80,6 +80,12 @@ export function checkBuild(themeName, out, isVendored = isVendoredPath) {
|
|
|
80
80
|
for (const tok of sortedUnique(text.match(TOKEN_RE) ?? [])) {
|
|
81
81
|
problems.push(`[${themeName}] unresolved token ${tok} in ${rel}`);
|
|
82
82
|
}
|
|
83
|
+
// `TOKEN_RE` cannot see a credit (colon, lowercase key): one left standing names no upstream.
|
|
84
|
+
if (text.includes('{{CREDIT:')) {
|
|
85
|
+
for (const tok of sortedUnique(text.match(CREDIT_RE) ?? ['{{CREDIT:'])) {
|
|
86
|
+
problems.push(`[${themeName}] unresolved token ${tok} in ${rel}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
83
89
|
for (const p of linkProblems(md, text, outAbs, rel)) problems.push(`[${themeName}] ${p}`);
|
|
84
90
|
for (const p of descriptionProblems(text, rel)) problems.push(`[${themeName}] ${p}`);
|
|
85
91
|
}
|
|
@@ -211,7 +217,13 @@ export function renderedProblems(bundle) {
|
|
|
211
217
|
else themeName = 'neutral';
|
|
212
218
|
let emit;
|
|
213
219
|
try { emit = readText(path.join(bundle, '.geneseed-emit')).trim(); } catch { emit = ''; }
|
|
214
|
-
const host =
|
|
220
|
+
const [host, scope] = EMIT_HOST_SCOPE.get(emit) ?? ['', ''];
|
|
221
|
+
// A Claude-shaped PROJECT install is not a bundle: its tree is `AGENTS.md`/`CLAUDE.md` plus a
|
|
222
|
+
// `.bob/`/`.claude/`/`.openclaude/` data dir, so every rendered file reads "missing" and no
|
|
223
|
+
// rebuild can clear it. The console's onboarding once put a Bob project install exactly here
|
|
224
|
+
// (`./Harness` under the checkout, reported 2026-10: 178 such lines). Its own drift is
|
|
225
|
+
// `geneseed diff`'s and `status`'s to report, not this committed-bundle check's.
|
|
226
|
+
if (scope === 'project' && CLAUDE_STYLE.includes(host)) return [];
|
|
215
227
|
let items;
|
|
216
228
|
try {
|
|
217
229
|
// THE FIRST CALLER IN THE PORT THAT CATCHES A REFUSAL, and that is why the stderr is
|
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
* is the reader that feeds it.
|
|
8
8
|
*/
|
|
9
9
|
import path from 'node:path';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
DOCS, DOC_FOLDERS, PLUGIN_SRC, RETIRED_KIT_IDS, ROOT, SRC, kitMembers, loadKits, makeCfg, ruleCanon, specLinks, upstreams,
|
|
12
|
+
} from '../build/source.mjs';
|
|
13
|
+
import { effectiveTheme, renderFile } from '../build/render.mjs';
|
|
11
14
|
import { themeFiles } from '../hosts/installs.mjs';
|
|
12
15
|
import { readText } from '../lib/fs.mjs';
|
|
13
16
|
import { formatRepr } from '../lib/json.mjs';
|
|
@@ -25,7 +28,7 @@ import { existsSync, readdirSync } from 'node:fs';
|
|
|
25
28
|
* so they drift silently — they already had. Pure over its inputs, so the corpus can feed it
|
|
26
29
|
* crafted drift without touching the tree.
|
|
27
30
|
*/
|
|
28
|
-
export function proseMirrorProblems(readme, web, counts, skillStems, shipped = '') {
|
|
31
|
+
export function proseMirrorProblems(readme, web, counts, skillStems, shipped = '', upstreamKeys = undefined) {
|
|
29
32
|
const problems = [];
|
|
30
33
|
const { laws, agents, skills, plugins } = counts;
|
|
31
34
|
|
|
@@ -68,6 +71,32 @@ export function proseMirrorProblems(readme, web, counts, skillStems, shipped = '
|
|
|
68
71
|
}
|
|
69
72
|
}
|
|
70
73
|
|
|
74
|
+
// The README Credits section names EXACTLY the `src/upstreams.json` keys, both directions,
|
|
75
|
+
// as the skills row does. A missing section is a problem, or deleting it would turn the gate
|
|
76
|
+
// green. `upstreamKeys` is optional, like `plugins`, because the corpus predates it.
|
|
77
|
+
if (upstreamKeys !== undefined) {
|
|
78
|
+
const lines = readme.split('\n').map((ln) => ln.replace(/\r$/, ''));
|
|
79
|
+
const at = lines.findIndex((ln) => ln.trim() === '## 🙏 Credits');
|
|
80
|
+
if (at < 0) {
|
|
81
|
+
problems.push("[authoring] README has no '## 🙏 Credits' section — src/upstreams.json has "
|
|
82
|
+
+ `${upstreamKeys.length} upstreams to credit`);
|
|
83
|
+
} else {
|
|
84
|
+
const end = lines.findIndex((ln, i) => i > at && ln.startsWith('## '));
|
|
85
|
+
const listed = new Set();
|
|
86
|
+
for (const ln of lines.slice(at + 1, end < 0 ? undefined : end).filter((l) => l.startsWith('- '))) {
|
|
87
|
+
const k = /^- `([^`]+)`/.exec(ln);
|
|
88
|
+
if (k) listed.add(k[1]);
|
|
89
|
+
else problems.push(`[authoring] README Credits bullet names no upstream key in backticks: '${ln}'`);
|
|
90
|
+
}
|
|
91
|
+
for (const key of [...upstreamKeys].filter((k) => !listed.has(k)).sort()) {
|
|
92
|
+
problems.push(`[authoring] README Credits omits upstream '${key}' (src/upstreams.json)`);
|
|
93
|
+
}
|
|
94
|
+
for (const key of [...listed].filter((k) => !upstreamKeys.includes(k)).sort()) {
|
|
95
|
+
problems.push(`[authoring] README Credits names '${key}', which src/upstreams.json does not define`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
71
100
|
for (const m of web.matchAll(/(\d+) universal (?:laws|Rules)/g)) {
|
|
72
101
|
if (Number(m[1]) !== laws) {
|
|
73
102
|
problems.push(`[authoring] docs/ prose says '${m[1]} universal laws/Rules' but `
|
|
@@ -127,8 +156,13 @@ export function countTableProblems() {
|
|
|
127
156
|
return [`[authoring] AGENT.md.tmpl unreadable: ${e.message}`];
|
|
128
157
|
}
|
|
129
158
|
|
|
159
|
+
// A kit member's row lives in its kit's manifest, not the template (spec A6). A manifest that
|
|
160
|
+
// does not parse is `kitProblems`' to name; here it means no kit rows.
|
|
161
|
+
let kits;
|
|
162
|
+
try { kits = loadKits(); } catch { kits = new Map(); }
|
|
163
|
+
const kitRows = [...kits.values()].flatMap((m) => [...(m.catalogue?.skills ?? []), ...(m.catalogue?.agents ?? [])]);
|
|
130
164
|
const linked = { agents: new Set(), skills: new Set() };
|
|
131
|
-
for (const m of ttext.matchAll(TMPL_SPEC_RE)) {
|
|
165
|
+
for (const m of [ttext, ...kitRows].join('\n').matchAll(TMPL_SPEC_RE)) {
|
|
132
166
|
if (m[2] !== '_template') linked[m[1] === 'AGENTS' ? 'agents' : 'skills'].add(m[2]);
|
|
133
167
|
}
|
|
134
168
|
for (const folder of ['agents', 'skills']) {
|
|
@@ -173,11 +207,14 @@ export function countTableProblems() {
|
|
|
173
207
|
// the SHIPPED.md triple count it. It is added HERE rather than in `srcStems`, whose other
|
|
174
208
|
// readers above — the AGENT.md table links and SKILL_CLASS — are about flat specs only.
|
|
175
209
|
const skillsDir = path.join(SRC, 'skills');
|
|
210
|
+
// Core only: a kit is off by default, so its members are in no badge, README list or SHIPPED triple.
|
|
211
|
+
const members = kitMembers(kits);
|
|
176
212
|
const shippedSkills = new Set([...skillFiles, ...(isDir(skillsDir) ? readdirSync(skillsDir) : [])
|
|
177
|
-
.filter((d) => !d.startsWith('_') && isFile(path.join(skillsDir, d, 'SKILL.md')))]
|
|
213
|
+
.filter((d) => !d.startsWith('_') && isFile(path.join(skillsDir, d, 'SKILL.md')))]
|
|
214
|
+
.filter((s) => !members.skills.has(s)));
|
|
178
215
|
|
|
179
216
|
const counts = {
|
|
180
|
-
agents: srcStems('agents').
|
|
217
|
+
agents: [...srcStems('agents')].filter((a) => !members.agents.has(a)).length,
|
|
181
218
|
skills: shippedSkills.size,
|
|
182
219
|
laws: lawNums.length,
|
|
183
220
|
// Every doctrine rule in every pack, opt-in ones included: the laws badge alone told a
|
|
@@ -242,6 +279,116 @@ export function countTableProblems() {
|
|
|
242
279
|
problems.push('[authoring] SHIPPED.md carries no \'N laws, N agents, N skills\' line — '
|
|
243
280
|
+ 'deleting it is how that gate goes green while the count drifts');
|
|
244
281
|
}
|
|
245
|
-
|
|
282
|
+
// A bad upstreams.json is creditProblems' to name; here it means no keys to hold the README to.
|
|
283
|
+
let upstreamKeys;
|
|
284
|
+
try { upstreamKeys = Object.keys(upstreams()); } catch { upstreamKeys = []; }
|
|
285
|
+
problems.push(...proseMirrorProblems(readme, web, counts, shippedSkills, shipped, upstreamKeys));
|
|
286
|
+
return problems;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** The `.md` files directly in `d`, sorted; none for a missing folder. */
|
|
290
|
+
const mdIn = (d) => { try { return readdirSync(d).filter((f) => f.endsWith('.md')).sort(); } catch { return []; } };
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* The kit arm (spec A6, R3, R4, B7): each `src/kits/<kit>.json` against `src`. Refused: a
|
|
294
|
+
* manifest that does not parse or lacks `description`/`rules`/`skills`/`agents`; a reserved kit
|
|
295
|
+
* id (`RETIRED_KIT_IDS`); `rules` naming a missing file, or rendering (neutral theme) to more
|
|
296
|
+
* words than its `rulesWordBudget` (default 300); a member in two kits, or a core one (a row in
|
|
297
|
+
* AGENT.md.tmpl) — R3; a member with no spec (or a vendored folder skill) or no `registry.json` row; a catalogue row not of the shape `| [<m>]({{DIR_*}}/<m>.md) | <text> |`, or a member without exactly one; any `banned` string (manifest) in a member file or the rules, unless the line carries `<!-- banned: ok -->`; and a core file linking a
|
|
298
|
+
* kit member, which dangles with the kit off — R4. `registry.json` sits beside `src`, as at ROOT.
|
|
299
|
+
*/
|
|
300
|
+
export function kitProblems(src = SRC) {
|
|
301
|
+
let kits;
|
|
302
|
+
try { kits = loadKits(src); } catch (e) { return [`[authoring] ${e.message}`]; }
|
|
303
|
+
if (!kits.size) return [];
|
|
304
|
+
const problems = [];
|
|
305
|
+
const core = { agents: new Set(), skills: new Set() };
|
|
306
|
+
let tmpl = '';
|
|
307
|
+
try { tmpl = readText(path.join(src, 'AGENT.md.tmpl')); } catch { /* countTableProblems names it */ }
|
|
308
|
+
for (const m of tmpl.matchAll(TMPL_SPEC_RE)) core[m[1] === 'AGENTS' ? 'agents' : 'skills'].add(m[2]);
|
|
309
|
+
// null = no usable registry: `registryProblems` already names that (and every member it lacks a
|
|
310
|
+
// row for), so a member's missing row is only reported here when there IS an entities object.
|
|
311
|
+
let registry = null;
|
|
312
|
+
try { registry = JSON.parse(readText(path.join(path.dirname(src), 'registry.json'))).entities ?? null; } catch {
|
|
313
|
+
// registryProblems names an unreadable registry
|
|
314
|
+
}
|
|
315
|
+
const owner = new Map();
|
|
316
|
+
const cfg = { ...makeCfg(), src };
|
|
317
|
+
let theme = null;
|
|
318
|
+
for (const [kit, m] of kits) {
|
|
319
|
+
const at = `src/kits/${kit}.json`;
|
|
320
|
+
if (Object.hasOwn(RETIRED_KIT_IDS, kit)) {
|
|
321
|
+
problems.push(`[authoring] ${at}: kit id '${kit}' is reserved (${RETIRED_KIT_IDS[kit]})`);
|
|
322
|
+
}
|
|
323
|
+
const lacking = ['description', 'rules', 'skills', 'agents'].filter((k) => (
|
|
324
|
+
k === 'skills' || k === 'agents' ? !Array.isArray(m[k]) : typeof m[k] !== 'string'));
|
|
325
|
+
for (const k of lacking) problems.push(`[authoring] ${at} has no '${k}'`);
|
|
326
|
+
if (lacking.length) continue;
|
|
327
|
+
for (const kind of ['skills', 'agents']) {
|
|
328
|
+
for (const name of m[kind]) {
|
|
329
|
+
const key = `${kind}/${name}`;
|
|
330
|
+
if (owner.has(key)) problems.push(`[authoring] ${key} is in two kits: '${owner.get(key)}' and '${kit}'`);
|
|
331
|
+
else owner.set(key, kit);
|
|
332
|
+
if (core[kind].has(name)) problems.push(`[authoring] ${key} is core (AGENT.md.tmpl lists it), but kit '${kit}' claims it`);
|
|
333
|
+
if (!isFile(path.join(src, kind, `${name}.md`))) {
|
|
334
|
+
problems.push(kind === 'skills' && isFile(path.join(src, kind, name, 'SKILL.md'))
|
|
335
|
+
? `[authoring] kit '${kit}' lists ${name}, a vendored folder skill; kit members must be flat specs`
|
|
336
|
+
: `[authoring] kit '${kit}' lists ${key}, but src/${key}.md does not exist`);
|
|
337
|
+
}
|
|
338
|
+
if (registry !== null && !Object.hasOwn(registry, key)) problems.push(`[authoring] ${key} (kit '${kit}') has no row in registry.json`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// A6: a kit's catalogue rows are its own members' — a core row here would show twice with the
|
|
342
|
+
// kit on, or vanish with it off. Each row is `| [<m>]({{DIR_*}}/<m>.md) | <text> |`, one per member.
|
|
343
|
+
for (const kind of ['skills', 'agents']) {
|
|
344
|
+
const dir = kind === 'skills' ? 'SKILLS' : 'AGENTS';
|
|
345
|
+
const seen = new Map();
|
|
346
|
+
for (const row of m.catalogue?.[kind] ?? []) {
|
|
347
|
+
const hit = new RegExp(`^\\| \\[([a-z0-9-]+)\\]\\(\\{\\{DIR_${dir}\\}\\}/\\1\\.md\\) \\| \\S.* \\|$`).exec(row);
|
|
348
|
+
const named = hit?.[1] ?? row.match(/\{\{DIR_(?:AGENTS|SKILLS)\}\}\/([A-Za-z0-9_-]+)\.md/)?.[1];
|
|
349
|
+
if (named && !m[kind].includes(named)) {
|
|
350
|
+
problems.push(`[authoring] ${at} catalogue lists ${kind}/${named}, which is not a member of kit '${kit}'`);
|
|
351
|
+
} else if (!hit) {
|
|
352
|
+
problems.push(`[authoring] ${at} catalogue.${kind} row is not \`| [<member>]({{DIR_${dir}}}/<member>.md) | <text> |\`: ${row}`);
|
|
353
|
+
} else seen.set(named, (seen.get(named) ?? 0) + 1);
|
|
354
|
+
}
|
|
355
|
+
for (const name of m[kind]) {
|
|
356
|
+
const n = seen.get(name) ?? 0;
|
|
357
|
+
if (n !== 1) problems.push(`[authoring] ${at} catalogue.${kind} has ${n} rows for ${name}, want exactly one`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
// T4: strings no member file may contain; a line carrying the marker is exempt (a rule that names the construct to forbid it).
|
|
361
|
+
const files = [m.rules, ...m.skills.flatMap((n) => [`skills/${n}.md`, ...mdIn(path.join(src, 'skills', n)).map((f) => `skills/${n}/${f}`)]),
|
|
362
|
+
...m.agents.map((n) => `agents/${n}.md`)];
|
|
363
|
+
for (const rel of files) {
|
|
364
|
+
if (!isFile(path.join(src, rel))) continue;
|
|
365
|
+
readText(path.join(src, rel)).split('\n').forEach((line, i) => {
|
|
366
|
+
if (line.includes('<!-- banned: ok -->')) return;
|
|
367
|
+
for (const b of m.banned ?? []) if (line.includes(b)) problems.push(`[authoring] src/${rel}:${i + 1} contains '${b}', banned by kit '${kit}'`);
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
const rules = path.join(src, m.rules);
|
|
371
|
+
if (!isFile(rules)) {
|
|
372
|
+
problems.push(`[authoring] ${at}: rules '${m.rules}' does not exist`);
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
theme ??= effectiveTheme(cfg, 'neutral');
|
|
376
|
+
const words = renderFile(cfg, rules, theme).split(/\s+/).filter(Boolean).length;
|
|
377
|
+
const budget = m.rulesWordBudget ?? 300;
|
|
378
|
+
if (words > budget) {
|
|
379
|
+
problems.push(`[authoring] src/${m.rules} renders to ${words} words, over kit '${kit}' rulesWordBudget of ${budget}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
// R4. A core member (R3, named above) is left out: every core file linking it would repeat that.
|
|
383
|
+
const r4 = new Set();
|
|
384
|
+
for (const l of specLinks(src)) {
|
|
385
|
+
// Links run kit -> core only: a core file, or ANOTHER kit's file, linking a member dangles
|
|
386
|
+
// whenever that member's kit is off.
|
|
387
|
+
if (!l.owner || l.kit === l.owner || core[l.kind].has(l.name)) continue;
|
|
388
|
+
r4.add(l.kit
|
|
389
|
+
? `[authoring] src/${l.rel} (kit '${l.kit}') links ${l.kind}/${l.name}, a member of kit '${l.owner}' — links run kit → core only`
|
|
390
|
+
: `[authoring] src/${l.rel} links ${l.kind}/${l.name}, a kit member — the link dangles when the kit is off`);
|
|
391
|
+
}
|
|
392
|
+
problems.push(...r4);
|
|
246
393
|
return problems;
|
|
247
394
|
}
|