geneseed 3.13.0 → 3.15.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 +93 -0
- package/SHIPPED.md +1 -1
- package/adapters/opencode/plugins/geneseed-context.js +23 -9
- package/adapters/opencode/plugins/geneseed-guard.js +16 -4
- package/adapters/opencode/plugins/geneseed-learn.js +45 -10
- package/adapters/opencode/plugins/geneseed-notify.js +37 -19
- package/adapters/opencode/plugins/geneseed-workflow.js +3 -1
- package/adapters/opencode/workflows/_runtime.js +16 -3
- package/bin/geneseed-cli.mjs +32 -34
- package/bin/geneseed-hook.mjs +28 -18
- package/docs/extending.md +5 -5
- package/harness.config.json +1 -1
- package/js/README.md +28 -15
- package/js/build/args.mjs +375 -0
- package/js/build/bundle.mjs +10 -10
- package/js/build/driver.mjs +145 -560
- package/js/build/emit-claude.mjs +5 -6
- package/js/build/emit-common.mjs +2 -14
- package/js/build/emit-opencode.mjs +12 -8
- package/js/build/generate.mjs +21 -14
- package/js/build/render.mjs +6 -10
- package/js/build/source.mjs +9 -17
- package/js/build/stubs.mjs +6 -5
- package/js/build/version.mjs +21 -2
- package/js/hosts/hooks-context.mjs +432 -0
- package/js/hosts/hooks-learn.mjs +275 -0
- package/js/hosts/hooks-prims.mjs +108 -0
- package/js/hosts/hooks.mjs +50 -977
- package/js/hosts/hosts.mjs +31 -25
- package/js/hosts/installs.mjs +38 -20
- package/js/hosts/link.mjs +75 -26
- package/js/hosts/mcp.mjs +12 -48
- package/js/hosts/memory-files.mjs +190 -0
- package/js/hosts/native.mjs +106 -48
- package/js/hosts/opencode.mjs +44 -36
- package/js/hosts/settings.mjs +135 -414
- package/js/hosts/shim.mjs +375 -0
- package/js/inspect/checks-authoring.mjs +21 -828
- package/js/inspect/checks-build.mjs +4 -4
- package/js/inspect/checks-constitution.mjs +493 -0
- package/js/inspect/checks-counts.mjs +242 -0
- package/js/inspect/checks-repo.mjs +17 -8
- package/js/inspect/diff.mjs +9 -7
- package/js/inspect/doctor.mjs +2 -2
- package/js/inspect/excludes.mjs +10 -5
- package/js/inspect/inventory.mjs +4 -4
- package/js/inspect/scan.mjs +1 -1
- package/js/inspect/status.mjs +19 -22
- package/js/inspect/validate.mjs +50 -37
- package/js/lib/fs.mjs +12 -11
- package/js/lib/json.mjs +4 -4
- package/js/lib/paths.mjs +27 -33
- package/js/lib/proc.mjs +3 -3
- package/js/lib/prompt.mjs +132 -0
- package/js/lib/text.mjs +18 -4
- package/js/lib/udiff.mjs +3 -2
- package/js/loop/catalog.mjs +5 -3
- package/js/loop/cli.mjs +26 -12
- package/js/loop/graph.mjs +1 -1
- package/js/loop/registry.mjs +10 -15
- package/js/loop/score.mjs +1 -1
- package/js/loop/state.mjs +35 -11
- package/js/maintain/memory.mjs +6 -7
- package/js/maintain/migrate.mjs +22 -33
- package/js/maintain/setup.mjs +9 -131
- package/js/maintain/uninstall.mjs +57 -97
- package/js/maintain/update.mjs +115 -34
- package/js/ui/anim.mjs +8 -12
- package/js/ui/cli.mjs +37 -43
- package/js/ui/menu.mjs +7 -13
- package/js/ui/tui.mjs +14 -123
- package/js/web/actions.mjs +109 -403
- package/js/web/activity.mjs +1 -1
- package/js/web/api.mjs +41 -725
- package/js/web/catalog.mjs +527 -0
- package/js/web/daemon.mjs +21 -3
- package/js/web/docs.mjs +29 -72
- package/js/web/handler.mjs +51 -27
- package/js/web/jobs.mjs +24 -2
- package/js/web/routes.mjs +61 -6
- package/js/web/server.mjs +4 -4
- package/js/web/state.mjs +138 -0
- package/js/web/user-files.mjs +339 -0
- package/package.json +1 -1
- package/web/dist/assets/Activity-kpWxd70F.js +1 -0
- package/web/dist/assets/ActivityDetail-Ccr942VL.js +1 -0
- package/web/dist/assets/Dashboard-D1lE7jcg.js +2 -0
- package/web/dist/assets/Docs-BHyaggFy.js +5 -0
- package/web/dist/assets/FilterInput-CVyqmYWM.js +1 -0
- package/web/dist/assets/GreenhouseView-Sc_UDMOE.js +2 -0
- package/web/dist/assets/Installs-Gw-pJ3Zi.js +2 -0
- package/web/dist/assets/JournalView-_gxpsxXi.js +1 -0
- package/web/dist/assets/Laws-S9fq1r9T.js +3 -0
- package/web/dist/assets/Library-MBxadRzn.js +1 -0
- package/web/dist/assets/Loops-Da_JhlzM.js +2 -0
- package/web/dist/assets/{Markdown-CdTX0kLe.js → Markdown-X2a3qvOh.js} +11 -11
- package/web/dist/assets/OperatorHudView--TkvDybH.js +2 -0
- package/web/dist/assets/Personal-CTXKHVbV.js +1 -0
- package/web/dist/assets/Tabs-B0cdf8k6.js +1 -0
- package/web/dist/assets/activity-Dr7ZMBI3.js +1 -0
- package/web/dist/assets/api-CxgfYu18.js +1 -0
- package/web/dist/assets/drawable-BbWQxe82.js +1 -0
- package/web/dist/assets/index-Ctp-4poP.css +1 -0
- package/web/dist/assets/index-ujKG1ZNf.js +2 -0
- package/web/dist/assets/{react-Bbzhmq4v.js → react-B9Z_tXRV.js} +1 -1
- package/web/dist/assets/sections-GalMS0Zv.js +1 -0
- package/web/dist/assets/usePoll-DD8VjtSg.js +1 -0
- package/web/dist/index.html +5 -6
- package/web/src/pages/Laws.jsx +0 -1
- package/web/dist/assets/Activity-Ca2Aruu7.js +0 -1
- package/web/dist/assets/ActivityDetail-Bo7v4Z_r.js +0 -1
- package/web/dist/assets/Dashboard-BuMq46aT.js +0 -2
- package/web/dist/assets/Docs-BzccCllG.js +0 -5
- package/web/dist/assets/FilterInput-CBcJBQPI.js +0 -1
- package/web/dist/assets/GreenhouseView-BCIKcnGG.js +0 -2
- package/web/dist/assets/Installs-DMFJvq7s.js +0 -2
- package/web/dist/assets/JournalView-o1rmd1_1.js +0 -1
- package/web/dist/assets/Laws-BANxq_I_.js +0 -3
- package/web/dist/assets/Library-DW4x0fbh.js +0 -1
- package/web/dist/assets/Loops-DEBibbBP.js +0 -2
- package/web/dist/assets/OperatorHudView-CLD0fYaA.js +0 -2
- package/web/dist/assets/Personal-6uMJvyBG.js +0 -1
- package/web/dist/assets/Tabs-BubnyZhF.js +0 -1
- package/web/dist/assets/activity-CObqyxkL.js +0 -1
- package/web/dist/assets/api-D_ixdyEY.js +0 -1
- package/web/dist/assets/drawable-C_x5xncq.js +0 -1
- package/web/dist/assets/index-CMPgezFm.js +0 -2
- package/web/dist/assets/index-DMsT4Aa6.css +0 -1
- package/web/dist/assets/rolldown-runtime-hePW80VL.js +0 -1
- package/web/dist/assets/sections-Dg23Hel5.js +0 -1
- package/web/dist/assets/usePoll-BOqeunzF.js +0 -1
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The COUNT gates — the hand-written tables, badges and prose that restate how many laws,
|
|
3
|
+
* agents, skills, themes and plugins ship, held against `src/`.
|
|
4
|
+
*
|
|
5
|
+
* Split out of `checks-authoring.mjs`, which still aggregates these (and re-exports them for
|
|
6
|
+
* its existing importers). `proseMirrorProblems` is pure over its inputs; `countTableProblems`
|
|
7
|
+
* is the reader that feeds it.
|
|
8
|
+
*/
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { DOCS, DOC_FOLDERS, PLUGIN_SRC, ROOT, SRC, ruleCanon } from '../build/source.mjs';
|
|
11
|
+
import { themeFiles } from '../hosts/installs.mjs';
|
|
12
|
+
import { readText } from '../lib/fs.mjs';
|
|
13
|
+
import { formatRepr } from '../lib/json.mjs';
|
|
14
|
+
import { lawMetaProblems } from './checks-constitution.mjs';
|
|
15
|
+
import { LAW_CLASS, LAW_CLASSES, SKILL_CLASS } from './inventory.mjs';
|
|
16
|
+
import { globSorted, has, isDir, isFile, srcStems } from './scan.mjs';
|
|
17
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* `_harness_build._prose_mirror_problems` — the human-readable count mirrors, the ones the
|
|
21
|
+
* badge regex never sees.
|
|
22
|
+
*
|
|
23
|
+
* The README "What you get" table and the web onboarding copy each restate the law / agent /
|
|
24
|
+
* skill counts in prose, and the README enumerates the skills by name. Nothing renders these,
|
|
25
|
+
* so they drift silently — they already had. Pure over its inputs, so the corpus can feed it
|
|
26
|
+
* crafted drift without touching the tree.
|
|
27
|
+
*/
|
|
28
|
+
export function proseMirrorProblems(readme, web, counts, skillStems, shipped = '') {
|
|
29
|
+
const problems = [];
|
|
30
|
+
const { laws, agents, skills, plugins } = counts;
|
|
31
|
+
|
|
32
|
+
for (const m of readme.matchAll(/(\d+) universal laws/g)) {
|
|
33
|
+
if (Number(m[1]) !== laws) {
|
|
34
|
+
problems.push(`[authoring] README prose says '${m[1]} universal laws' but src has ${laws}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// The README plugin count. It had drifted — the overview sentence said six while seven
|
|
38
|
+
// shipped — because the project's other plugin count ({N_PLUGINS} in the web docs) reads
|
|
39
|
+
// a DEPLOYED plugins dir that a markdown file in this repository never has. `plugins` is
|
|
40
|
+
// optional in `counts` (Python's `.get`, undefined here) because the corpus predates it.
|
|
41
|
+
if (plugins !== undefined) {
|
|
42
|
+
for (const m of readme.matchAll(/(\d+) \*{0,2}plugins/g)) {
|
|
43
|
+
if (Number(m[1]) !== plugins) {
|
|
44
|
+
problems.push(`[authoring] README prose says '${m[1]} plugins' but `
|
|
45
|
+
+ `adapters/opencode/plugins has ${plugins}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
for (const [label, want] of [['Agents', agents], ['Skills', skills]]) {
|
|
50
|
+
const m = new RegExp(`${label}\\*\\*\\s*\\((\\d+)\\)`).exec(readme);
|
|
51
|
+
if (m && Number(m[1]) !== want) {
|
|
52
|
+
problems.push(`[authoring] README '${label} (${m[1]})' but src has ${want}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// The README Skills row must enumerate EXACTLY the source skills — a dropped name is
|
|
56
|
+
// invisible to the (N) count alone, which is the off-by-one this gate forbids.
|
|
57
|
+
const row = readme.split('\n').find((ln) => ln.includes('🛠') && ln.includes('Skills')) ?? '';
|
|
58
|
+
if (row.includes('workflows:')) {
|
|
59
|
+
const listed = new Set(row.split('workflows:').slice(1).join('workflows:').split('·')
|
|
60
|
+
.map((seg) => seg.replace(/[^a-z0-9-]/g, '')));
|
|
61
|
+
listed.delete('');
|
|
62
|
+
for (const missing of [...skillStems].filter((s) => !listed.has(s)).sort()) {
|
|
63
|
+
problems.push(`[authoring] README skills list omits '${missing}'`);
|
|
64
|
+
}
|
|
65
|
+
for (const orphan of [...listed].filter((s) => !skillStems.has(s)).sort()) {
|
|
66
|
+
problems.push(`[authoring] README skills list names '${orphan}' but no `
|
|
67
|
+
+ `skills/${orphan}.md exists`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
for (const m of web.matchAll(/(\d+) universal (?:laws|Rules)/g)) {
|
|
72
|
+
if (Number(m[1]) !== laws) {
|
|
73
|
+
problems.push(`[authoring] docs/ prose says '${m[1]} universal laws/Rules' but `
|
|
74
|
+
+ `src has ${laws}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
for (const m of web.matchAll(/(\d+) capability specialists/g)) {
|
|
78
|
+
if (Number(m[1]) !== agents) {
|
|
79
|
+
problems.push(`[authoring] docs/ prose says '${m[1]} capability specialists' but `
|
|
80
|
+
+ `src has ${agents}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// N is a curated-subset size, not the total, so it is gated against the wikilinks it
|
|
84
|
+
// introduces rather than against the skill count. `[\s\S]` is Python's `re.S` dot.
|
|
85
|
+
const m = /(\d+) repeatable workflows the agent can invoke by name([\s\S]*?)playbook under/
|
|
86
|
+
.exec(web);
|
|
87
|
+
if (m) {
|
|
88
|
+
const listedN = (m[2].match(/\[\[[^\]]+\]\]/g) ?? []).length;
|
|
89
|
+
if (Number(m[1]) !== listedN) {
|
|
90
|
+
problems.push(`[authoring] docs/ says '${m[1]} repeatable workflows' `
|
|
91
|
+
+ `but its wikilink list has ${listedN}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ⚠ THE ABSENCE OF THIS TRIPLE IS CHECKED BY THE CALLER, NOT HERE. This arm is satisfiable by
|
|
96
|
+
// DELETING THE SENTENCE — no match, no problem, green — which is a gate failing open. The
|
|
97
|
+
// presence check lives in `countTableProblems`, beside the read that already knows whether
|
|
98
|
+
// the file could be opened at all; an empty `shipped` here means "nothing to compare".
|
|
99
|
+
const s = /(\d+) laws, (\d+) agents, (\d+) skills/.exec(shipped);
|
|
100
|
+
if (s) {
|
|
101
|
+
for (const [got, want, label] of [[s[1], laws, 'laws'], [s[2], agents, 'agents'],
|
|
102
|
+
[s[3], skills, 'skills']]) {
|
|
103
|
+
if (Number(got) !== want) {
|
|
104
|
+
problems.push(`[authoring] SHIPPED.md says '${got} ${label}' but src has ${want}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return problems;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** `_build_render._TMPL_SPEC_RE`, as the count gate spells it. */
|
|
112
|
+
const TMPL_SPEC_RE = /\{\{DIR_(AGENTS|SKILLS)\}\}\/([A-Za-z0-9_-]+)\.md/g;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* `_harness_build._count_table_problems` — the hand-authored AGENT.md tables and the README
|
|
116
|
+
* badges, against `src/`.
|
|
117
|
+
*
|
|
118
|
+
* The tables must list EXACTLY the spec files (no dead row, no orphaned spec) and each
|
|
119
|
+
* `agents`/`skills`/`laws`/`themes` badge must equal the real count. This is the
|
|
120
|
+
* authoring-time guarantee that lets tables, badges and prose stay hand-written without
|
|
121
|
+
* silently drifting from the source tree.
|
|
122
|
+
*/
|
|
123
|
+
export function countTableProblems() {
|
|
124
|
+
const problems = [];
|
|
125
|
+
let ttext;
|
|
126
|
+
try { ttext = readText(path.join(SRC, 'AGENT.md.tmpl')); } catch (e) {
|
|
127
|
+
return [`[authoring] AGENT.md.tmpl unreadable: ${e.message}`];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const linked = { agents: new Set(), skills: new Set() };
|
|
131
|
+
for (const m of ttext.matchAll(TMPL_SPEC_RE)) {
|
|
132
|
+
if (m[2] !== '_template') linked[m[1] === 'AGENTS' ? 'agents' : 'skills'].add(m[2]);
|
|
133
|
+
}
|
|
134
|
+
for (const folder of ['agents', 'skills']) {
|
|
135
|
+
const files = srcStems(folder);
|
|
136
|
+
for (const missing of [...linked[folder]].filter((x) => !files.has(x)).sort()) {
|
|
137
|
+
problems.push(`[authoring] AGENT.md links ${folder}/${missing}.md but no such spec exists`);
|
|
138
|
+
}
|
|
139
|
+
for (const orphan of [...files].filter((x) => !linked[folder].has(x)).sort()) {
|
|
140
|
+
problems.push(`[authoring] ${folder}/${orphan}.md exists but the AGENT.md table omits it`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const skillFiles = srcStems('skills');
|
|
145
|
+
for (const missing of [...skillFiles].filter((s) => !has(SKILL_CLASS, s)).sort()) {
|
|
146
|
+
problems.push(`[authoring] skills/${missing}.md has no category in SKILL_CLASS`);
|
|
147
|
+
}
|
|
148
|
+
for (const stale of Object.keys(SKILL_CLASS).filter((s) => !skillFiles.has(s)).sort()) {
|
|
149
|
+
problems.push(`[authoring] SKILL_CLASS lists '${stale}' but no skills/${stale}.md exists`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// The numerals the laws RENDER with — positional, off the canon — because LAW_CLASS and the
|
|
153
|
+
// console's LAW_META are keyed the way the rendered catalogue numbers them.
|
|
154
|
+
const lawRules = ruleCanon(SRC).rules.filter((r) => r.kind === 'LAW');
|
|
155
|
+
const lawNums = lawRules.map((r) => r.label);
|
|
156
|
+
for (const num of lawNums) {
|
|
157
|
+
if (!has(LAW_CLASS, num)) {
|
|
158
|
+
problems.push(`[authoring] laws/universal.md rule ${num} has no class in LAW_CLASS`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// Sorted by the Roman numeral as a STRING; `doctorCollect` re-sorts every message anyway.
|
|
162
|
+
for (const num of Object.keys(LAW_CLASS).sort()) {
|
|
163
|
+
if (!LAW_CLASSES.includes(LAW_CLASS[num])) {
|
|
164
|
+
problems.push(`[authoring] LAW_CLASS['${num}'] = '${LAW_CLASS[num]}' is not a known `
|
|
165
|
+
+ `class ${formatRepr(LAW_CLASSES)}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
problems.push(...lawMetaProblems(lawRules, LAW_CLASS, LAW_CLASSES));
|
|
169
|
+
|
|
170
|
+
// WHAT SHIPS, NOT WHAT IS FLAT. A folder skill (`src/skills/<name>/SKILL.md`, vendored with
|
|
171
|
+
// its scripts or references) ships exactly like a flat one, so the badge, the README list and
|
|
172
|
+
// the SHIPPED.md triple count it. It is added HERE rather than in `srcStems`, whose other
|
|
173
|
+
// readers above — the AGENT.md table links and SKILL_CLASS — are about flat specs only.
|
|
174
|
+
const skillsDir = path.join(SRC, 'skills');
|
|
175
|
+
const shippedSkills = new Set([...skillFiles, ...(isDir(skillsDir) ? readdirSync(skillsDir) : [])
|
|
176
|
+
.filter((d) => !d.startsWith('_') && isFile(path.join(skillsDir, d, 'SKILL.md')))]);
|
|
177
|
+
|
|
178
|
+
const counts = {
|
|
179
|
+
agents: srcStems('agents').size,
|
|
180
|
+
skills: shippedSkills.size,
|
|
181
|
+
laws: lawNums.length,
|
|
182
|
+
themes: themeFiles().length,
|
|
183
|
+
plugins: existsSync(PLUGIN_SRC)
|
|
184
|
+
? readdirSync(PLUGIN_SRC).filter((f) => f.startsWith('geneseed-') && f.endsWith('.js')).length
|
|
185
|
+
: 0,
|
|
186
|
+
};
|
|
187
|
+
let readme;
|
|
188
|
+
try { readme = readText(path.join(ROOT, 'README.md')); } catch { return problems; }
|
|
189
|
+
// ⚠ PRESENCE FIRST, THEN THE VALUE — the same absence arm the SHIPPED triple needed, for the
|
|
190
|
+
// same reason: `if (m && …)` is green when there is no badge, so deleting a badge silences its
|
|
191
|
+
// own gate.
|
|
192
|
+
for (const [key, n] of Object.entries(counts)) {
|
|
193
|
+
const m = new RegExp(`badge/${key}-(\\d+)`).exec(readme);
|
|
194
|
+
if (!m) {
|
|
195
|
+
problems.push(`[authoring] README has no ${key} badge — deleting one is how this gate `
|
|
196
|
+
+ 'goes green while the reader is told nothing');
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (Number(m[1]) !== n) {
|
|
200
|
+
problems.push(`[authoring] README ${key} badge says ${m[1]} but src has ${n}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// WHERE THE ONBOARDING COPY LIVES, AND WHY THIS READ MOVED. It used to open the one module
|
|
205
|
+
// that held the web console's onboarding prose. That prose has since moved into
|
|
206
|
+
// `docs/<folder>/*.md`, and the counts in it render from `{N_LAWS}` / `{N_AGENTS}` / `{N_SKILLS}`
|
|
207
|
+
// — so the read was still succeeding against a file the sentences had left, all three arms
|
|
208
|
+
// below scored zero, and the check looked healthy while gating nothing. A templated count
|
|
209
|
+
// cannot drift; what these arms still catch is a maintainer typing the NUMBER into a page
|
|
210
|
+
// instead of the token, which is the drift that reaches a reader.
|
|
211
|
+
//
|
|
212
|
+
// `web` stays fail-soft — a missing docs tree is not an authoring fault.
|
|
213
|
+
let web = '';
|
|
214
|
+
try {
|
|
215
|
+
web = DOC_FOLDERS
|
|
216
|
+
.flatMap((f) => (isDir(path.join(DOCS, f))
|
|
217
|
+
? globSorted(path.join(DOCS, f), (n) => n.endsWith('.md')) : []))
|
|
218
|
+
.map(readText).join('\n');
|
|
219
|
+
} catch { web = ''; }
|
|
220
|
+
// ⚠ NOT FAIL-SOFT, UNLIKE `web` ABOVE, AND THE ASYMMETRY IS THE POINT. A missing docs tree
|
|
221
|
+
// is not an authoring fault — a checkout can legitimately lack it. SHIPPED.md is a tracked,
|
|
222
|
+
// shipped file whose CONTENT this function asserts, so an unreadable one was
|
|
223
|
+
// indistinguishable from a deleted claim: swallowing it into `''` handed the triple arm an
|
|
224
|
+
// empty string and turned a hard read failure into a silent pass.
|
|
225
|
+
//
|
|
226
|
+
// AND THE PRESENCE OF THE TRIPLE IS CHECKED HERE rather than inside `proseMirrorProblems`:
|
|
227
|
+
// this is the caller that already owns the question "could the file be read".
|
|
228
|
+
let shipped;
|
|
229
|
+
try {
|
|
230
|
+
shipped = readText(path.join(ROOT, 'SHIPPED.md'));
|
|
231
|
+
} catch (e) {
|
|
232
|
+
problems.push(`[authoring] SHIPPED.md is unreadable (${e.message}) — the counts it carries `
|
|
233
|
+
+ 'cannot be checked, and a gate that cannot read its subject must say so');
|
|
234
|
+
shipped = '';
|
|
235
|
+
}
|
|
236
|
+
if (shipped && !/(\d+) laws, (\d+) agents, (\d+) skills/.test(shipped)) {
|
|
237
|
+
problems.push('[authoring] SHIPPED.md carries no \'N laws, N agents, N skills\' line — '
|
|
238
|
+
+ 'deleting it is how that gate goes green while the count drifts');
|
|
239
|
+
}
|
|
240
|
+
problems.push(...proseMirrorProblems(readme, web, counts, shippedSkills, shipped));
|
|
241
|
+
return problems;
|
|
242
|
+
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { PLUGIN_SRC, ROOT, SRC, THEMES, WORKFLOW_SRC } from '../build/source.mjs';
|
|
11
11
|
import { VENDORED_SKILL_DIRS } from '../hosts/native.mjs';
|
|
12
|
-
import { hookShimPath, shimDeadPaths } from '../hosts/
|
|
12
|
+
import { hookShimPath, shimDeadPaths } from '../hosts/shim.mjs';
|
|
13
13
|
import { readText } from '../lib/fs.mjs';
|
|
14
14
|
import { formatRepr, formatValue } from '../lib/json.mjs';
|
|
15
15
|
import { splitLines } from '../lib/udiff.mjs';
|
|
@@ -313,11 +313,20 @@ export function moduleMapProblems() {
|
|
|
313
313
|
+ 'checks-repo.mjs asserts it describes every module under js/'];
|
|
314
314
|
}
|
|
315
315
|
|
|
316
|
-
// A row is `| `name.mjs` | …
|
|
317
|
-
//
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
316
|
+
// A row is `| `name.mjs` | … |`, and it documents `<folder>/name.mjs` for the folder whose
|
|
317
|
+
// `## `js/<folder>/`` heading it sits under. Keyed by basename alone, a row answered for every
|
|
318
|
+
// folder holding that name — catalog.mjs, cli.mjs and registry.mjs each live in two — so a
|
|
319
|
+
// deleted module stayed "documented" by its namesake's row. Reading the CODE SPAN and not the
|
|
320
|
+
// whole line keeps a module named in a "Before editing" paragraph from counting as a row.
|
|
321
|
+
const documented = new Set();
|
|
322
|
+
let folder = null;
|
|
323
|
+
for (const line of text.split('\n')) {
|
|
324
|
+
const heading = /^## `js\/([a-z0-9-]+)\/`/.exec(line);
|
|
325
|
+
if (heading) { folder = heading[1]; continue; }
|
|
326
|
+
if (line.startsWith('## ')) { folder = null; continue; }
|
|
327
|
+
const row = /^\|\s*`([a-z0-9-]+\.mjs)`\s*\|/.exec(line);
|
|
328
|
+
if (row && folder) documented.add(`${folder}/${row[1]}`);
|
|
329
|
+
}
|
|
321
330
|
|
|
322
331
|
const onDisk = new Set();
|
|
323
332
|
const jsDir = path.join(String(ROOT), 'js');
|
|
@@ -328,7 +337,7 @@ export function moduleMapProblems() {
|
|
|
328
337
|
// that only knew `/` finds ONE part on Windows and therefore zero modules — which is what
|
|
329
338
|
// the vacuity guard below caught the first time this ran.
|
|
330
339
|
const parts = path.relative(jsDir, rel).replaceAll('\\', '/').split('/');
|
|
331
|
-
if (parts.length === 2) onDisk.add(parts
|
|
340
|
+
if (parts.length === 2) onDisk.add(parts.join('/'));
|
|
332
341
|
}
|
|
333
342
|
|
|
334
343
|
if (onDisk.size < 30) {
|
|
@@ -344,7 +353,7 @@ export function moduleMapProblems() {
|
|
|
344
353
|
}
|
|
345
354
|
for (const mod of [...documented].sort()) {
|
|
346
355
|
if (!onDisk.has(mod)) {
|
|
347
|
-
problems.push(`[authoring] js/README.md has a row for
|
|
356
|
+
problems.push(`[authoring] js/README.md has a row for js/${mod}, which does not exist — the map `
|
|
348
357
|
+ 'sends a reader to a file that is not there');
|
|
349
358
|
}
|
|
350
359
|
}
|
package/js/inspect/diff.mjs
CHANGED
|
@@ -22,11 +22,10 @@
|
|
|
22
22
|
* * **The diff algorithm.** A cell can produce ONE edited file and see one hunk. It cannot
|
|
23
23
|
* produce the shapes where `difflib`'s choices differ from any other diff's — ties, the
|
|
24
24
|
* recursion's alignment, and above all `autojunk`, which only engages at 200 lines and
|
|
25
|
-
* changes the hunks on every real harness file.
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* switched off, so the corpus is not describing a constant).
|
|
25
|
+
* changes the hunks on every real harness file. The former Python suite gated it as a
|
|
26
|
+
* corpus, measured in both directions (28 of its 176 cases disagreed when `autojunk` was
|
|
27
|
+
* switched off). That corpus is gone and no Node test drives `unifiedDiff` directly, so
|
|
28
|
+
* beyond the one hunk the `diff/--full-…` cell sees, nothing pins these shapes today.
|
|
30
29
|
* * **The timestamp.** `_write_improvements` names the file `improvements-%Y%m%d-%H%M%S.md`
|
|
31
30
|
* and stamps the report with `datetime.now()`, so the two implementations cannot write the
|
|
32
31
|
* same bytes across a second boundary — and a cell runs them in sequence, so the boundary
|
|
@@ -45,7 +44,7 @@ import {
|
|
|
45
44
|
GLOBAL_MANIFEST, VERSION_MARKER, expanduser, opencodeConfigDir, resolvePath,
|
|
46
45
|
} from '../hosts/hosts.mjs';
|
|
47
46
|
import {
|
|
48
|
-
EMIT_HOST_SCOPE, defaultTheme, doctrinesForBuild, footprintOfDir, modeOfDir, postureOfDir,
|
|
47
|
+
EMIT_HOST_SCOPE, defaultTheme, doctrinesForBuild, excludedRulesOfDir, footprintOfDir, modeOfDir, postureOfDir,
|
|
49
48
|
trustOfDir,
|
|
50
49
|
readJsonMaybe, readMaybe, themeOfDir,
|
|
51
50
|
} from '../hosts/installs.mjs';
|
|
@@ -177,7 +176,10 @@ export function diffCollect({ target = null, theme = null, emit = null, footprin
|
|
|
177
176
|
// makes explicit rather than leaving to `makeCfg`'s parameter default two layers down.
|
|
178
177
|
posture: postureOfDir(dir), mode: modeOfDir(dir), doctrines: doctrinesForBuild(dir),
|
|
179
178
|
// The loop skill's preset, or its `Default trust preset:` line reads as edited.
|
|
180
|
-
trust: trustOfDir(dir),
|
|
179
|
+
trust: trustOfDir(dir, host),
|
|
180
|
+
// The excluded rules, or an install built `--exclude-rules "process 5"` reads its
|
|
181
|
+
// carrier as drift: `expected` would re-state the rule the deployment took away.
|
|
182
|
+
excludeRules: excludedRulesOfDir(dir),
|
|
181
183
|
}));
|
|
182
184
|
// The one place a render depends on WHERE it is installed: a global Bob emit writes each
|
|
183
185
|
// folder skill's absolute directory into its SKILL.md (see `writeNativeLayer`), so the
|
package/js/inspect/doctor.mjs
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
import path from 'node:path';
|
|
33
33
|
import { emitGlobalInto, emitProjectInto, main as driverMain } from '../build/driver.mjs';
|
|
34
34
|
import { ROOT } from '../build/source.mjs';
|
|
35
|
-
import { resolvePath } from '../hosts/hosts.mjs';
|
|
35
|
+
import { CLAUDE_STYLE, resolvePath } from '../hosts/hosts.mjs';
|
|
36
36
|
import { installedDefaults, themeFiles } from '../hosts/installs.mjs';
|
|
37
37
|
import { validateIsVendored } from '../hosts/native.mjs';
|
|
38
38
|
import { printOut } from '../lib/fs.mjs';
|
|
@@ -143,7 +143,7 @@ export function globalEmitProblems(themeName) {
|
|
|
143
143
|
* skills one level deeper than a bundle does.
|
|
144
144
|
*/
|
|
145
145
|
export function claudeBobEmitProblems(themeName) {
|
|
146
|
-
return emitProblemsOver(
|
|
146
|
+
return emitProblemsOver(CLAUDE_STYLE, (tmp, label) => {
|
|
147
147
|
const root = path.join(tmp, 'root');
|
|
148
148
|
return {
|
|
149
149
|
label: `${themeName} ${label}`,
|
package/js/inspect/excludes.mjs
CHANGED
|
@@ -78,14 +78,19 @@ const canon = (p) => resolvePath(p);
|
|
|
78
78
|
/**
|
|
79
79
|
* `_same` — is this stored entry the same folder as `repo`?
|
|
80
80
|
*
|
|
81
|
-
* `
|
|
82
|
-
* `
|
|
83
|
-
*
|
|
84
|
-
*
|
|
81
|
+
* Resolved like `repo` itself (`canon`: `..` collapsed, symlinks followed), the one rule the
|
|
82
|
+
* hook's `sovereignBypass` and the OpenCode plugins apply too — so an entry that EXCLUDES a
|
|
83
|
+
* folder is exactly the entry `exclude remove` can take back. It used to compare lexically,
|
|
84
|
+
* mirroring the former Python `Path()`, which made a hand-edited `<repo>/../repo` an entry the
|
|
85
|
+
* plugins honoured, the hook ignored, and `remove` could not find.
|
|
85
86
|
*/
|
|
86
87
|
function same(stored, repo) {
|
|
87
88
|
if (typeof stored !== 'string' || !stored) return false;
|
|
88
|
-
|
|
89
|
+
try {
|
|
90
|
+
return normcase(canon(stored)) === normcase(repo);
|
|
91
|
+
} catch {
|
|
92
|
+
return false; // a `~user` form `expanduser` refuses names no folder
|
|
93
|
+
}
|
|
89
94
|
}
|
|
90
95
|
|
|
91
96
|
/**
|
package/js/inspect/inventory.mjs
CHANGED
|
@@ -34,10 +34,10 @@ import { parseJson } from '../lib/json.mjs';
|
|
|
34
34
|
* `_harness_tui.LAW_HEADING_RE` — a heading in the RENDERED laws file, e.g.
|
|
35
35
|
* `### Rule IV — Deletion Is Deliberate`.
|
|
36
36
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
37
|
+
* The rendered side only: the unrendered source declares a rule as `{{LAW:<id>}} <Name>`, and
|
|
38
|
+
* `ruleCanon` in `js/build/source.mjs` is what reads that.
|
|
39
39
|
*/
|
|
40
|
-
|
|
40
|
+
const LAW_HEADING_RE = /^###\s+\S+\s+([IVXLCDM]+)\s+[—-]\s+(.+?)\s*$/;
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
43
|
* A heading in a RENDERED doctrine pack, e.g. `### Doctrine craft 1 — Automate Repetition`.
|
|
@@ -52,7 +52,7 @@ export const LAW_HEADING_RE = /^###\s+\S+\s+([IVXLCDM]+)\s+[—-]\s+(.+?)\s*$/;
|
|
|
52
52
|
* `([a-z]+)` refuses `Rule I`'s uppercase numeral, and `([IVXLCDM]+)` refuses a lowercase pack
|
|
53
53
|
* id. Measured across all fourteen themes — zero cross-matches either way.
|
|
54
54
|
*/
|
|
55
|
-
|
|
55
|
+
const DOCTRINE_HEADING_RE = /^###\s+\S+\s+([a-z]+)\s+(\d+)\s+[—-]\s+(.+?)\s*$/;
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
58
|
* A section heading in the RENDERED ontology, e.g. `#### Telos`.
|
package/js/inspect/scan.mjs
CHANGED
|
@@ -36,7 +36,7 @@ const FENCE_RE = /```[\s\S]*?```/g;
|
|
|
36
36
|
const INLINE_CODE_RE = /`[^`]*`/g;
|
|
37
37
|
const COMMENT_RE = /<!--[\s\S]*?-->/g;
|
|
38
38
|
|
|
39
|
-
/** `_harness_core.strip_code` — fenced blocks,
|
|
39
|
+
/** `_harness_core.strip_code` — fenced blocks, then HTML comments, then inline code, in that order. */
|
|
40
40
|
export function stripCode(text) {
|
|
41
41
|
return text.replace(FENCE_RE, '').replace(COMMENT_RE, '').replace(INLINE_CODE_RE, '');
|
|
42
42
|
}
|
package/js/inspect/status.mjs
CHANGED
|
@@ -34,25 +34,26 @@
|
|
|
34
34
|
* became the second and third. Nothing about them changed in the move — the 166-cell
|
|
35
35
|
* acceptance matrix is what licensed attempting it, `status`'s own eleven cells among them.
|
|
36
36
|
*
|
|
37
|
-
* WHAT NO CELL CAN REACH, and where it is gated instead.
|
|
38
|
-
* status/version section
|
|
37
|
+
* WHAT NO CELL CAN REACH, and where it is gated instead. The former Python matrix's
|
|
38
|
+
* status/version section carried the long form; the short form is that ROOT is not
|
|
39
39
|
* redirectable across a process boundary, so three things here are unreachable from any
|
|
40
40
|
* cell: `versionVerdict`'s "up to date" (needs a marker holding a fingerprint no cell can
|
|
41
41
|
* know), `accentFor`'s cyan fallback (an unknown theme is refused upstream by
|
|
42
42
|
* `effectiveTheme`), and the whole ANSI half of `statusLines` (`_color_enabled` is
|
|
43
43
|
* `sys.stdout.isatty()`, and every harness captures stdout through a pipe). All three are
|
|
44
|
-
* PURE FUNCTIONS of their arguments, so
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
44
|
+
* PURE FUNCTIONS of their arguments, so the former Python suite gated all three as a corpus
|
|
45
|
+
* instead — a third answer to the colour question, which had been posed as a choice between
|
|
46
|
+
* shipping it ungated and not shipping it. That corpus is gone: `tests/unit/lifecycle.test.mjs`
|
|
47
|
+
* now calls `versionVerdict` and both halves of `statusLines` directly, and nothing pins
|
|
48
|
+
* `accentFor`'s fallback.
|
|
49
49
|
*
|
|
50
50
|
* `manifestIsClaude` was a FOURTH until wave 2 of the P0/P1 review: it is only reached for
|
|
51
51
|
* a candidate with no known host, and `ROOT/"Harness"` was ordered ahead of the sandbox's
|
|
52
52
|
* own. Both ROOT-relative bundle candidates are gone from this walk and from
|
|
53
53
|
* `installedDefaults`, so the only no-known-host candidate left is `cwd/"Harness"`, which
|
|
54
54
|
* is inside the sandbox. It is reachable now; no cell seeds a manifest without an emit
|
|
55
|
-
* marker, so it is
|
|
55
|
+
* marker, so it is gated only by the direct calls in `tests/unit/claude.test.mjs`, and that is
|
|
56
|
+
* a gap rather than a wall.
|
|
56
57
|
*/
|
|
57
58
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
58
59
|
import path from 'node:path';
|
|
@@ -66,19 +67,15 @@ import {
|
|
|
66
67
|
} from '../hosts/hosts.mjs';
|
|
67
68
|
// P5f moved the install DETECTORS out of this file — `diff` renders its expected copy in the
|
|
68
69
|
// deployed theme and footprint, and `rebuild-all` re-emits in the deployed everything, so
|
|
69
|
-
// three verbs now read them.
|
|
70
|
-
// re-exported below because `tests/fixtures/pure_probe.mjs` names this module for two of them
|
|
71
|
-
// and a corpus that followed the code to its new file would stop testing the caller's view.
|
|
70
|
+
// three verbs now read them.
|
|
72
71
|
import {
|
|
73
|
-
defaultTheme, installedDefaults, installTargets,
|
|
72
|
+
defaultTheme, installedDefaults, installTargets, readJsonMaybe, readMaybe,
|
|
74
73
|
} from '../hosts/installs.mjs';
|
|
75
74
|
import { installProfile, rebuildCommand } from '../build/generate.mjs';
|
|
76
|
-
import { shimDead } from '../hosts/
|
|
75
|
+
import { shimDead } from '../hosts/shim.mjs';
|
|
77
76
|
import { printOut } from '../lib/fs.mjs';
|
|
78
77
|
import { codePointLength, padEndToWidth } from '../lib/text.mjs';
|
|
79
78
|
|
|
80
|
-
export { defaultTheme, manifestIsClaude };
|
|
81
|
-
|
|
82
79
|
// ---- version ---------------------------------------------------------------------------
|
|
83
80
|
|
|
84
81
|
/** `_harness_status._version_verdict`. */
|
|
@@ -331,13 +328,13 @@ export function statusLines(d, color = false) {
|
|
|
331
328
|
+ `(${d.facts} fact${d.facts === 1 ? '' : 's'})`],
|
|
332
329
|
['version', `${d.installed_fp || '(none)'} ${DOT} source ${d.source_fp}`],
|
|
333
330
|
];
|
|
334
|
-
// ⚠ A ROW, NOT A WIDER `components` LINE
|
|
335
|
-
//
|
|
336
|
-
// `
|
|
337
|
-
//
|
|
338
|
-
//
|
|
339
|
-
//
|
|
340
|
-
//
|
|
331
|
+
// ⚠ A ROW, NOT A WIDER `components` LINE. When this landed, `statusLines` had 30 recorded
|
|
332
|
+
// cases per platform in the former Python suite's corpus, every one holding the box's exact
|
|
333
|
+
// bytes — including `17 agents · 47 skills · 37 laws` — and no recorder left to re-bless them.
|
|
334
|
+
// Touching `components` would have moved all 30; adding a row would too, since the box width
|
|
335
|
+
// is measured over `rows`. So the row is CONDITIONAL on a key those arguments did not carry,
|
|
336
|
+
// exactly as `agent_md` below already is. The corpus is gone; a caller without the key still
|
|
337
|
+
// gets the box it got before.
|
|
341
338
|
//
|
|
342
339
|
// It carries the two tiers `components` cannot: `components` counts the invariants as
|
|
343
340
|
// `laws`, which is still true, and says nothing about the ontology or the packs.
|
package/js/inspect/validate.mjs
CHANGED
|
@@ -9,48 +9,36 @@
|
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { buildInto, emitGlobalInto, emitProjectInto } from '../build/driver.mjs';
|
|
11
11
|
import { resolvePath } from '../hosts/hosts.mjs';
|
|
12
|
+
import {
|
|
13
|
+
doctrinesOfDir, excludedRulesOfDir, modeOfDir, postureOfDir,
|
|
14
|
+
} from '../hosts/installs.mjs';
|
|
12
15
|
import { validateIsVendored } from '../hosts/native.mjs';
|
|
13
|
-
import { printOut, printErr
|
|
16
|
+
import { printOut, printErr } from '../lib/fs.mjs';
|
|
14
17
|
import { comparePaths } from '../lib/paths.mjs';
|
|
15
18
|
import { stripWhitespace } from '../lib/text.mjs';
|
|
16
|
-
import {
|
|
19
|
+
import { checkBuild } from './checks-build.mjs';
|
|
17
20
|
import { cmdDoctor } from './doctor.mjs';
|
|
18
|
-
import {
|
|
21
|
+
import { isDir, isFile, rglob, withTempDir, within } from './scan.mjs';
|
|
19
22
|
|
|
20
23
|
// --------------------------------------------------------------------------------------
|
|
21
24
|
// validate — the generator's `--validate-only`, on this binary because it runs the doctor
|
|
22
25
|
// --------------------------------------------------------------------------------------
|
|
23
26
|
|
|
24
27
|
/**
|
|
25
|
-
* `
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* A NEAR-TWIN OF `checkBuild` ABOVE, AND DELIBERATELY NOT IT, because the reference is two
|
|
29
|
-
* functions and they differ in three observable ways: the token list is `sorted(set(...))`
|
|
30
|
-
* here and bare `set(...)` there, the message carries no `[theme]` prefix (the caller adds
|
|
31
|
-
* `[emit]` instead), and an unreadable file is SKIPPED rather than raised. Python duplicates
|
|
32
|
-
* the loop because `build.py` cannot import the harness tree; here the two live in one file
|
|
33
|
-
* and the two now sit one file apart (`checkBuild` is in `checks-build.mjs`); the
|
|
34
|
-
* duplication is eight lines — every primitive underneath (`rglob`, `stripCode`,
|
|
35
|
-
* `linkProblems`, `readText`) is the shared one, which is the half that could actually drift.
|
|
28
|
+
* `validate --help`. Its own text, because the parser it borrows is the generator's and that
|
|
29
|
+
* parser's `-h` prints `usage: geneseed-build …` — naming `--sync-themes`/`--config-dir`, which
|
|
30
|
+
* this verb refuses, under a program name the user did not type.
|
|
36
31
|
*/
|
|
37
|
-
export
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
for (const tok of sortedUnique(text.match(TOKEN_RE) ?? [])) {
|
|
48
|
-
problems.push(`unresolved token ${tok} in ${rel}`);
|
|
49
|
-
}
|
|
50
|
-
problems.push(...linkProblems(md, text, out, rel));
|
|
51
|
-
}
|
|
52
|
-
return problems;
|
|
53
|
-
}
|
|
32
|
+
export const VALIDATE_USAGE = [
|
|
33
|
+
'usage: geneseed validate [-h] [--theme THEME] [--emit EMIT] [--out OUT] [--root ROOT]',
|
|
34
|
+
' [--footprint {lean,full}] [--posture POSTURE] [--mode MODE]',
|
|
35
|
+
' [--trust TRUST] [--doctrines DOCTRINES] [--exclude-rules RULES] [-v]',
|
|
36
|
+
'',
|
|
37
|
+
"Render and emit into a throwaway sandbox, run the doctor's checks, and write nothing real.",
|
|
38
|
+
"Takes the generator's render flags (see geneseed-build --help) except --sync-themes and",
|
|
39
|
+
'--config-dir.',
|
|
40
|
+
'',
|
|
41
|
+
].join('\n');
|
|
54
42
|
|
|
55
43
|
/**
|
|
56
44
|
* `subprocess.run(..., capture_output=True, text=True)` around an IN-PROCESS call.
|
|
@@ -109,6 +97,17 @@ function captureStreams(fn) {
|
|
|
109
97
|
* contract absolutely — this verb is deliberately outside the verb table, so nothing else does.
|
|
110
98
|
*/
|
|
111
99
|
export function cmdValidate(args) {
|
|
100
|
+
// Two generator flags with no meaning in a dry run, refused rather than silently ignored:
|
|
101
|
+
// `--sync-themes` WRITES into the live themes dir, and `--config-dir` names a real install dir
|
|
102
|
+
// while every render here goes to a sandbox — accepting either would claim a check of
|
|
103
|
+
// something this verb never touches.
|
|
104
|
+
for (const [flag, set] of [['--sync-themes', args.syncThemes], ['--config-dir', args.cfgDir]]) {
|
|
105
|
+
if (set) {
|
|
106
|
+
printErr(`geneseed: error: validate does not take ${flag} — it renders into a sandbox `
|
|
107
|
+
+ 'and writes nothing real; run `geneseed validate --help` for what it takes\n');
|
|
108
|
+
return 2;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
112
111
|
const problems = [];
|
|
113
112
|
const scan = withTempDir((tmpRaw) => {
|
|
114
113
|
// `.resolve()` mirrors `_check_build`'s: on Windows a temp dir can come back in 8.3
|
|
@@ -122,7 +121,13 @@ export function cmdValidate(args) {
|
|
|
122
121
|
const sandbox = args.root ? path.join(root, 'bundle') : root;
|
|
123
122
|
const cfgDir = path.join(tmp, 'cfg');
|
|
124
123
|
const emit = args.emit;
|
|
125
|
-
|
|
124
|
+
// THE FIVE RENDER AXES TRAVEL WITH THE DRY RUN. They were parsed and then dropped, so
|
|
125
|
+
// `validate --doctrines craft` checked an all-packs render — a pass that said nothing
|
|
126
|
+
// about the build the operator was about to run.
|
|
127
|
+
const opts = {
|
|
128
|
+
theme: args.theme, footprint: args.footprint, posture: args.posture, mode: args.mode,
|
|
129
|
+
trust: args.trust, doctrines: args.doctrines, excludeRules: args.excludeRules,
|
|
130
|
+
};
|
|
126
131
|
let scanDirs;
|
|
127
132
|
try {
|
|
128
133
|
if (emit.endsWith('-global')) {
|
|
@@ -144,8 +149,8 @@ export function cmdValidate(args) {
|
|
|
144
149
|
// which ends `raise SystemExit(res.get("exit", 1))` — an INTEGER — so every refusal
|
|
145
150
|
// that crosses the seam arrives here as `SystemExit(1)` and `str(e)` is `'1'`, not the
|
|
146
151
|
// sentence `load_theme` wrote on stderr on the way out. A port that printed `e.message`
|
|
147
|
-
// says `unknown theme 'x'` where the reference says `1`. The
|
|
148
|
-
//
|
|
152
|
+
// says `unknown theme 'x'` where the reference says `1`. The former Python
|
|
153
|
+
// cross-implementation corpus is what found it; nothing else could.
|
|
149
154
|
if (e && e.exitCode !== undefined) {
|
|
150
155
|
printOut(`[validate-only] render/emit FAILED for theme '${args.theme}' `
|
|
151
156
|
+ `emit '${emit}': ${e.exitCode}\n`);
|
|
@@ -158,6 +163,14 @@ export function cmdValidate(args) {
|
|
|
158
163
|
.sort(comparePaths);
|
|
159
164
|
printOut(`[validate-only] theme=${args.theme} emit=${emit} `
|
|
160
165
|
+ `footprint=${args.footprint}\n`);
|
|
166
|
+
// READ BACK OFF THE RENDER, not echoed from `args`: this line is the proof the axes reached
|
|
167
|
+
// the sandbox, which an echo of the flags would print just as happily if they had not.
|
|
168
|
+
const at = scanDirs[0];
|
|
169
|
+
const packs = doctrinesOfDir(at);
|
|
170
|
+
const excluded = excludedRulesOfDir(at);
|
|
171
|
+
printOut(`[validate-only] rendered at posture=${postureOfDir(at) ?? '?'} `
|
|
172
|
+
+ `mode=${modeOfDir(at) ?? '?'} packs=${packs ? packs.join(',') || 'none' : '?'} `
|
|
173
|
+
+ `excluded=${excluded.map((id) => id.replace('.', ' ')).join(', ') || 'none'}\n`);
|
|
161
174
|
printOut(`[validate-only] would write ${written.length} file(s) under ${args.out}`
|
|
162
175
|
+ (args.root ? ` (root ${args.root})` : '')
|
|
163
176
|
+ ' — nothing was actually written (sandboxed).\n');
|
|
@@ -170,9 +183,9 @@ export function cmdValidate(args) {
|
|
|
170
183
|
printOut(` would write: ${path.relative(base, p)}\n`);
|
|
171
184
|
}
|
|
172
185
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
186
|
+
// The doctor's own per-tree scan, prefixed with the emit instead of a theme, and with the
|
|
187
|
+
// vendored shape every host depth can produce — a sandbox may hold a per-repo native layer.
|
|
188
|
+
for (const d of scanDirs) problems.push(...checkBuild(emit, d, validateIsVendored));
|
|
176
189
|
return scanDirs;
|
|
177
190
|
}, 'geneseed-validate-');
|
|
178
191
|
if (scan === null) return 1;
|