@rungs/cli 0.1.2 → 0.2.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/README.md +45 -8
- package/dist/cli.js +883 -127
- package/dist/cli.js.map +4 -4
- package/modules/README.md +13 -0
- package/modules/adr/gates/adr.toml +14 -2
- package/modules/adr/module.toml +19 -1
- package/modules/audit/module.toml +1 -0
- package/modules/backlog/gates/ids.toml +33 -0
- package/modules/backlog/module.toml +61 -1
- package/modules/ci/module.toml +1 -0
- package/modules/concurrency/module.toml +2 -0
- package/modules/design-sync/module.toml +2 -0
- package/modules/doc-authority/module.toml +4 -0
- package/modules/findings/module.toml +3 -0
- package/modules/gates/gates/structural.toml +61 -17
- package/modules/gates/module.toml +5 -0
- package/modules/instructions/module.toml +4 -0
- package/modules/release/gates/release.toml +43 -0
- package/modules/release/module.toml +16 -1
- package/modules/release/skills/cut-release/SKILL.md +8 -1
- package/modules/session/module.toml +4 -2
- package/modules/skills/module.toml +3 -0
- package/modules/specs/module.toml +4 -0
- package/modules/workflows/module.toml +2 -0
- package/package.json +1 -1
- package/src/add.ts +64 -2
- package/src/backlog.ts +182 -0
- package/src/check.ts +47 -4
- package/src/cli.ts +297 -27
- package/src/engines.ts +261 -13
- package/src/engines2.ts +69 -3
- package/src/engines3.ts +147 -0
- package/src/explain.ts +189 -0
- package/src/lifecycle.ts +90 -3
- package/src/manifest.ts +13 -1
- package/src/selftest.ts +221 -0
- package/src/types.ts +34 -0
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
5
|
-
import { dirname as
|
|
5
|
+
import { dirname as dirname8, join as join13, resolve as resolve4 } from "node:path";
|
|
6
6
|
|
|
7
7
|
// src/manifest.ts
|
|
8
8
|
import { readdirSync as readdirSync2, readFileSync, statSync as statSync2 } from "node:fs";
|
|
@@ -106,6 +106,7 @@ function loadManifest(dir) {
|
|
|
106
106
|
params: raw.params ?? {},
|
|
107
107
|
gates: raw.gates ?? [],
|
|
108
108
|
detect: raw.detect ?? {},
|
|
109
|
+
skills: raw.skills ?? {},
|
|
109
110
|
provenance: raw.provenance,
|
|
110
111
|
threshold: raw.threshold,
|
|
111
112
|
dir
|
|
@@ -161,6 +162,13 @@ function auditModules(mods) {
|
|
|
161
162
|
if (!g.why?.trim()) {
|
|
162
163
|
issues.push({ module: mod.name, kind: "gate-no-why", detail: `gate '${g.id}' has no 'why'` });
|
|
163
164
|
}
|
|
165
|
+
if (g.kind === "declared" && !g.applicability) {
|
|
166
|
+
issues.push({
|
|
167
|
+
module: mod.name,
|
|
168
|
+
kind: "gate-no-applicability",
|
|
169
|
+
detail: `gate '${g.id}' does not declare applicability (repo-content | our-artifacts | our-schema)`
|
|
170
|
+
});
|
|
171
|
+
}
|
|
164
172
|
}
|
|
165
173
|
}
|
|
166
174
|
return issues;
|
|
@@ -209,9 +217,9 @@ function resolveParams(mods, overrides = {}, repoRoot) {
|
|
|
209
217
|
}
|
|
210
218
|
return out;
|
|
211
219
|
}
|
|
212
|
-
function markers(
|
|
220
|
+
function markers(targetPath2, module, version) {
|
|
213
221
|
const hash = /\.(toml|ya?ml|gitignore|gitattributes|sh|ps1|conf|properties)$|(^|\/)\.(gitignore|gitattributes)$/.test(
|
|
214
|
-
|
|
222
|
+
targetPath2
|
|
215
223
|
);
|
|
216
224
|
return hash ? { begin: `# rungs:begin ${module}@${version}`, end: `# rungs:end ${module}` } : { begin: `<!-- rungs:begin ${module}@${version} -->`, end: `<!-- rungs:end ${module} -->` };
|
|
217
225
|
}
|
|
@@ -225,8 +233,8 @@ function mergeBlock(existing, fragment, module) {
|
|
|
225
233
|
const after = existing.slice(e.index + e[0].length);
|
|
226
234
|
return `${before}${fragment.trim()}${after}`;
|
|
227
235
|
}
|
|
228
|
-
const
|
|
229
|
-
return `${existing}${
|
|
236
|
+
const sep3 = existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
|
|
237
|
+
return `${existing}${sep3}${fragment.trim()}
|
|
230
238
|
`;
|
|
231
239
|
}
|
|
232
240
|
|
|
@@ -267,7 +275,7 @@ function addModule(mod, repoRoot, params, opts = {}) {
|
|
|
267
275
|
const base = join3(mod.dir, "skills");
|
|
268
276
|
const dir = opts.skillsDir ?? ".claude/skills";
|
|
269
277
|
for (const rel of walk(base)) {
|
|
270
|
-
write(`${dir}/${rel}`, sub(readFileSync2(join3(base, rel), "utf8")), "skill");
|
|
278
|
+
write(`${dir}/${rel}`, withOptedInExtensions(mod, rel, sub(readFileSync2(join3(base, rel), "utf8"))), "skill");
|
|
271
279
|
}
|
|
272
280
|
}
|
|
273
281
|
if (has("fragments")) {
|
|
@@ -346,6 +354,18 @@ var gateEntry = (mod) => (g) => {
|
|
|
346
354
|
if (g.why) lines.push(`why = """${g.why.trim()}"""`);
|
|
347
355
|
return lines.join("\n");
|
|
348
356
|
};
|
|
357
|
+
function blockedByParadigm(order, paradigms) {
|
|
358
|
+
const blocked = /* @__PURE__ */ new Map();
|
|
359
|
+
for (const mod of order) {
|
|
360
|
+
if (paradigms.has(mod.name)) {
|
|
361
|
+
blocked.set(mod.name, mod.name);
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
const dep = mod.requires.find((d) => blocked.has(d));
|
|
365
|
+
if (dep) blocked.set(mod.name, blocked.get(dep));
|
|
366
|
+
}
|
|
367
|
+
return blocked;
|
|
368
|
+
}
|
|
349
369
|
function resolveInstallOrder(requested, all) {
|
|
350
370
|
const byName = new Map(all.map((m) => [m.name, m]));
|
|
351
371
|
const order = [];
|
|
@@ -379,6 +399,19 @@ function resolveInstallOrder(requested, all) {
|
|
|
379
399
|
}
|
|
380
400
|
var contentHash = (s) => createHash("sha256").update(s.replace(/\r\n/g, "\n")).digest("hex").slice(0, 12);
|
|
381
401
|
var SHARED = /* @__PURE__ */ new Set(["AGENTS.md", "CLAUDE.md", ".gitignore", ".gitattributes", ".ai/gates.toml"]);
|
|
402
|
+
function withOptedInExtensions(mod, rel, content) {
|
|
403
|
+
const name = rel.split(/[\\/]/)[0];
|
|
404
|
+
const extensions = mod.skills?.[name]?.extensions;
|
|
405
|
+
if (!extensions || !Object.keys(extensions).length) return content;
|
|
406
|
+
const m = content.match(/^---\n([\s\S]*?)\n---/);
|
|
407
|
+
if (!m) return content;
|
|
408
|
+
const added = Object.entries(extensions).filter(([k]) => !new RegExp(`^${k}:`, "m").test(m[1])).map(([k, v]) => `${k}: ${v}`);
|
|
409
|
+
if (!added.length) return content;
|
|
410
|
+
return content.replace(/^---\n[\s\S]*?\n---/, `---
|
|
411
|
+
${m[1]}
|
|
412
|
+
${added.join("\n")}
|
|
413
|
+
---`);
|
|
414
|
+
}
|
|
382
415
|
function emittedFiles(mod, params, skillsDir = ".claude/skills") {
|
|
383
416
|
const out = /* @__PURE__ */ new Map();
|
|
384
417
|
const sub = (t) => substitute(t, mod.name, params);
|
|
@@ -392,7 +425,9 @@ function emittedFiles(mod, params, skillsDir = ".claude/skills") {
|
|
|
392
425
|
for (const rel of walk(base)) {
|
|
393
426
|
const target = sub(prefix + rel).split("\\").join("/");
|
|
394
427
|
if (SHARED.has(target)) continue;
|
|
395
|
-
|
|
428
|
+
let content = sub(readFileSync2(join3(base, rel), "utf8"));
|
|
429
|
+
if (dir === "skills") content = withOptedInExtensions(mod, rel, content);
|
|
430
|
+
out.set(target, content);
|
|
396
431
|
}
|
|
397
432
|
}
|
|
398
433
|
return out;
|
|
@@ -816,21 +851,136 @@ function writeReport(repoRoot, entries, harnesses, stamp) {
|
|
|
816
851
|
// src/check.ts
|
|
817
852
|
import { appendFileSync, existsSync as existsSync7, readFileSync as readFileSync8 } from "node:fs";
|
|
818
853
|
import { execSync as execSync3 } from "node:child_process";
|
|
819
|
-
import { dirname as
|
|
854
|
+
import { dirname as dirname5, join as join10 } from "node:path";
|
|
820
855
|
import { fileURLToPath } from "node:url";
|
|
821
856
|
import { parse as parse2 } from "smol-toml";
|
|
822
857
|
|
|
823
858
|
// src/engines.ts
|
|
824
859
|
import { existsSync as existsSync6, readFileSync as readFileSync7 } from "node:fs";
|
|
825
|
-
import { join as
|
|
860
|
+
import { join as join9, dirname as dirname4, resolve as resolve2 } from "node:path";
|
|
861
|
+
import { parse as parseToml } from "smol-toml";
|
|
862
|
+
|
|
863
|
+
// src/selftest.ts
|
|
864
|
+
import { mkdirSync as mkdirSync3, mkdtempSync, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
865
|
+
import { tmpdir } from "node:os";
|
|
866
|
+
import { dirname as dirname3, join as join6 } from "node:path";
|
|
867
|
+
function targetPath(table) {
|
|
868
|
+
const t = Array.isArray(table) ? table[0] ?? {} : table ?? {};
|
|
869
|
+
const pattern = t.file ?? [t.scan ?? []].flat()[0] ?? "**/*.md";
|
|
870
|
+
if (!pattern.includes("*")) return pattern;
|
|
871
|
+
return pattern.replace(/\*\*\//g, "probe/").replace(/\/\*\*/g, "/probe").replace(/\*/g, "probe").replace(/probe\.probe$/, "probe.md");
|
|
872
|
+
}
|
|
873
|
+
function build(root, table, fx, input) {
|
|
874
|
+
const write = (rel, body) => {
|
|
875
|
+
const full = join6(root, rel);
|
|
876
|
+
mkdirSync3(dirname3(full), { recursive: true });
|
|
877
|
+
writeFileSync3(full, body);
|
|
878
|
+
return rel;
|
|
879
|
+
};
|
|
880
|
+
if (typeof input === "string") return [write(targetPath(table), `${input}
|
|
881
|
+
`)];
|
|
882
|
+
if (!fx || typeof fx !== "object") return null;
|
|
883
|
+
if (Array.isArray(fx.fragments) && typeof fx.version === "string") {
|
|
884
|
+
const dir = fx.dir ?? "changelog.d";
|
|
885
|
+
const written = fx.fragments.map((n) => write(`${dir}/${n}`, `# ${n}
|
|
886
|
+
`));
|
|
887
|
+
written.push(write("package.json", JSON.stringify({ version: fx.version })));
|
|
888
|
+
return written;
|
|
889
|
+
}
|
|
890
|
+
if (typeof fx.matching_files === "number") {
|
|
891
|
+
const base = fx.location ?? dirname3(targetPath(table));
|
|
892
|
+
const marker = fx.exempt ? `<!-- ${fx.exempt} -->
|
|
893
|
+
` : "";
|
|
894
|
+
return Array.from(
|
|
895
|
+
{ length: fx.matching_files },
|
|
896
|
+
(_, i) => write(join6(base === "." ? "" : base, `probe-${i}.md`), `${marker}# probe ${i}
|
|
897
|
+
`)
|
|
898
|
+
);
|
|
899
|
+
}
|
|
900
|
+
const contentKeys = ["frontmatter", "sections", "opening", "body", "row", "table"];
|
|
901
|
+
if (contentKeys.some((k) => k in fx)) {
|
|
902
|
+
const parts = [];
|
|
903
|
+
if (fx.table) parts.push(`## ${fx.table}`, "");
|
|
904
|
+
if (fx.frontmatter && typeof fx.frontmatter === "object") {
|
|
905
|
+
parts.push("---");
|
|
906
|
+
for (const [k, v] of Object.entries(fx.frontmatter)) parts.push(`${k}: ${v}`);
|
|
907
|
+
parts.push("---", "");
|
|
908
|
+
}
|
|
909
|
+
if (fx.opening) parts.push(String(fx.opening), "");
|
|
910
|
+
for (const s of fx.sections ?? []) parts.push(`## ${s}`, "", "text", "");
|
|
911
|
+
if (fx.row && typeof fx.row === "object") {
|
|
912
|
+
const cols = Object.keys(fx.row);
|
|
913
|
+
parts.push(
|
|
914
|
+
`| ${cols.join(" | ")} |`,
|
|
915
|
+
`| ${cols.map(() => "---").join(" | ")} |`,
|
|
916
|
+
`| ${cols.map((c2) => fx.row[c2]).join(" | ")} |`,
|
|
917
|
+
""
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
if (fx.body) parts.push(String(fx.body), "");
|
|
921
|
+
return [write(fx.file ?? targetPath(table), `${parts.join("\n")}
|
|
922
|
+
`)];
|
|
923
|
+
}
|
|
924
|
+
return null;
|
|
925
|
+
}
|
|
926
|
+
var CONTEXT_FREE = /* @__PURE__ */ new Set([
|
|
927
|
+
"frontmatter-schema",
|
|
928
|
+
"sections",
|
|
929
|
+
"file-budget",
|
|
930
|
+
"register-schema",
|
|
931
|
+
"file-population",
|
|
932
|
+
"changelog-freshness"
|
|
933
|
+
]);
|
|
934
|
+
function deparam(spec, dir) {
|
|
935
|
+
const walk3 = (v) => typeof v === "string" ? v.replace(/\{\{[^}]+\}\}/g, dir) : Array.isArray(v) ? v.map(walk3) : v && typeof v === "object" ? Object.fromEntries(Object.entries(v).map(([k, x]) => [k, walk3(x)])) : v;
|
|
936
|
+
return walk3(spec);
|
|
937
|
+
}
|
|
938
|
+
function runSelfTests(gateId, engine, table, blocks) {
|
|
939
|
+
const out = [];
|
|
940
|
+
for (const b of blocks) {
|
|
941
|
+
const expect = b.expect === "fail" ? "fail" : "pass";
|
|
942
|
+
if (!CONTEXT_FREE.has(engine)) {
|
|
943
|
+
out.push({ gate: gateId, expect, outcome: "unrun", detail: `${engine} fixtures need context the fixture does not carry` });
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
if (!(engine in ENGINES)) {
|
|
947
|
+
out.push({ gate: gateId, expect, outcome: "unrun", detail: `engine '${engine}' not implemented` });
|
|
948
|
+
continue;
|
|
949
|
+
}
|
|
950
|
+
const root = mkdtempSync(join6(tmpdir(), "rungs-selftest-"));
|
|
951
|
+
try {
|
|
952
|
+
const files = build(root, table, b.fixture, b.input);
|
|
953
|
+
let spec = b.fixture?.opted_in ? Array.isArray(table) ? table.map((s) => ({ ...s, extensions_opted_in: b.fixture.opted_in })) : { ...table, extensions_opted_in: b.fixture.opted_in } : table;
|
|
954
|
+
if (Array.isArray(b.fixture?.fragments)) spec = deparam(spec, b.fixture.dir ?? "changelog.d");
|
|
955
|
+
if (!files) {
|
|
956
|
+
out.push({ gate: gateId, expect, outcome: "unrun", detail: `no builder for fixture ${JSON.stringify(b.fixture).slice(0, 60)}` });
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
let findings;
|
|
960
|
+
try {
|
|
961
|
+
findings = ENGINES[engine](spec, root, files).findings;
|
|
962
|
+
} catch (e) {
|
|
963
|
+
out.push({ gate: gateId, expect, outcome: "unrun", detail: `engine threw: ${e.message}`.slice(0, 90) });
|
|
964
|
+
continue;
|
|
965
|
+
}
|
|
966
|
+
const fired = findings.length > 0;
|
|
967
|
+
out.push(
|
|
968
|
+
fired === (expect === "fail") ? { gate: gateId, expect, outcome: "ok" } : { gate: gateId, expect, outcome: "mismatch", detail: `expected ${expect}, ${fired ? `fired: ${findings[0].message}`.slice(0, 80) : "did not fire"}` }
|
|
969
|
+
);
|
|
970
|
+
} finally {
|
|
971
|
+
rmSync(root, { recursive: true, force: true });
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
return out;
|
|
975
|
+
}
|
|
826
976
|
|
|
827
977
|
// src/engines2.ts
|
|
828
978
|
import { readFileSync as readFileSync5 } from "node:fs";
|
|
829
979
|
import { execSync } from "node:child_process";
|
|
830
|
-
import { join as
|
|
980
|
+
import { join as join7 } from "node:path";
|
|
831
981
|
var read = (root, rel) => {
|
|
832
982
|
try {
|
|
833
|
-
return readFileSync5(
|
|
983
|
+
return readFileSync5(join7(root, rel), "utf8");
|
|
834
984
|
} catch {
|
|
835
985
|
return "";
|
|
836
986
|
}
|
|
@@ -924,40 +1074,44 @@ var renderFreshness = (t, root, files) => {
|
|
|
924
1074
|
var registerSchema = (t, root, files) => {
|
|
925
1075
|
const findings = [];
|
|
926
1076
|
let examined = 0;
|
|
927
|
-
const
|
|
928
|
-
for (const
|
|
929
|
-
const
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
if (
|
|
933
|
-
const
|
|
934
|
-
|
|
935
|
-
(
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
examined++;
|
|
944
|
-
for (const [key, values] of Object.entries(t.enum ?? {})) {
|
|
945
|
-
const v = strip(row[key]);
|
|
946
|
-
if (v && !values.map(String).includes(v)) {
|
|
947
|
-
findings.push({ file: rel, message: `${key}='${v}' not one of ${values.join(", ")}` });
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
for (const c2 of t.non_empty ?? []) {
|
|
951
|
-
if (!strip(row[c2])) findings.push({ file: rel, message: `row ${firstCell(row)}: '${c2}' is empty` });
|
|
1077
|
+
const specs = [t, ...Object.values(t).filter((v) => v && typeof v === "object" && !Array.isArray(v) && v.table)];
|
|
1078
|
+
for (const t2 of specs) {
|
|
1079
|
+
const targets = t2.file ?? specs[0].file ? [t2.file ?? specs[0].file] : expand(files, t2.scan);
|
|
1080
|
+
for (const rel of targets) {
|
|
1081
|
+
const text = read(root, rel);
|
|
1082
|
+
if (!text) continue;
|
|
1083
|
+
for (const table of parseTables(text)) {
|
|
1084
|
+
const heading = sectionOf(text, table.headerLine).replace(/^#+\s*/, "").trim().toLowerCase();
|
|
1085
|
+
if (t2.table && !heading.startsWith(String(t2.table).toLowerCase())) continue;
|
|
1086
|
+
const cols = t2.required_cols ?? t2.table_columns ?? [];
|
|
1087
|
+
const present = cols.filter(
|
|
1088
|
+
(c2) => table.headers.some((h) => h.toLowerCase() === String(c2).toLowerCase())
|
|
1089
|
+
);
|
|
1090
|
+
if (cols.length && present.length < Math.max(2, Math.ceil(cols.length / 2))) continue;
|
|
1091
|
+
for (const c2 of cols) {
|
|
1092
|
+
if (!present.includes(c2)) findings.push({ file: rel, message: `register table missing column '${c2}'` });
|
|
952
1093
|
}
|
|
953
|
-
for (const
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
for (const
|
|
957
|
-
const v = strip(row[
|
|
958
|
-
if (
|
|
959
|
-
|
|
960
|
-
|
|
1094
|
+
for (const row of table.rows) {
|
|
1095
|
+
if (Object.values(row).every((v) => !v || v === "\u2014")) continue;
|
|
1096
|
+
examined++;
|
|
1097
|
+
for (const [key, values] of Object.entries(t2.enum ?? {})) {
|
|
1098
|
+
const v = strip(row[key]);
|
|
1099
|
+
if (v && !values.map(String).includes(v)) {
|
|
1100
|
+
findings.push({ file: rel, message: `${key}='${v}' not one of ${values.join(", ")}` });
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
for (const c2 of t2.non_empty ?? []) {
|
|
1104
|
+
if (!strip(row[c2])) findings.push({ file: rel, message: `row ${firstCell(row)}: '${c2}' is empty` });
|
|
1105
|
+
}
|
|
1106
|
+
for (const cond of t2.conditional ?? []) {
|
|
1107
|
+
const matches = Object.entries(cond.when ?? {}).every(([k, v]) => strip(row[k]) === String(v));
|
|
1108
|
+
if (!matches) continue;
|
|
1109
|
+
for (const c2 of cond.non_empty ?? []) {
|
|
1110
|
+
const v = strip(row[c2]);
|
|
1111
|
+
if (!v) findings.push({ file: rel, message: `row ${firstCell(row)}: '${c2}' required when ${JSON.stringify(cond.when)}` });
|
|
1112
|
+
else if (cond.min_words?.[c2] && v.split(/\s+/).length < cond.min_words[c2]) {
|
|
1113
|
+
findings.push({ file: rel, message: `row ${firstCell(row)}: '${c2}' is too thin to be a reason` });
|
|
1114
|
+
}
|
|
961
1115
|
}
|
|
962
1116
|
}
|
|
963
1117
|
}
|
|
@@ -1073,6 +1227,16 @@ var crossReference = (t, root, files) => {
|
|
|
1073
1227
|
}
|
|
1074
1228
|
return { findings, examined: skills.length };
|
|
1075
1229
|
};
|
|
1230
|
+
function landedWork(root, branch, base) {
|
|
1231
|
+
const git = (cmd2) => execSync(`git ${cmd2}`, { cwd: root, stdio: "pipe" }).toString().trim();
|
|
1232
|
+
try {
|
|
1233
|
+
const tip = git(`rev-parse ${branch}`);
|
|
1234
|
+
if (tip === git(`rev-parse ${base}`)) return false;
|
|
1235
|
+
return git(`log ${base} --merges --format=%P`).split("\n").some((line) => line.trim().split(/\s+/).slice(1).includes(tip));
|
|
1236
|
+
} catch {
|
|
1237
|
+
return true;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1076
1240
|
var gitStatusReconcile = (t, root, files) => {
|
|
1077
1241
|
const findings = [];
|
|
1078
1242
|
let merged;
|
|
@@ -1094,7 +1258,7 @@ var gitStatusReconcile = (t, root, files) => {
|
|
|
1094
1258
|
const status = text.match(new RegExp(`^${t.status_field ?? "status"}:\\s*(\\S+)`, "m"))?.[1];
|
|
1095
1259
|
if (!branch || !status) continue;
|
|
1096
1260
|
examined++;
|
|
1097
|
-
if (merged.has(branch) && (t.pre_review_statuses ?? []).includes(status)) {
|
|
1261
|
+
if (merged.has(branch) && (t.pre_review_statuses ?? []).includes(status) && landedWork(root, branch, t.integration_branch ?? "main")) {
|
|
1098
1262
|
findings.push({ file: rel, message: `branch ${branch} is merged but status is '${status}'` });
|
|
1099
1263
|
}
|
|
1100
1264
|
}
|
|
@@ -1137,16 +1301,54 @@ var computedClaim = (t, root, files) => {
|
|
|
1137
1301
|
// src/engines3.ts
|
|
1138
1302
|
import { readFileSync as readFileSync6 } from "node:fs";
|
|
1139
1303
|
import { execSync as execSync2 } from "node:child_process";
|
|
1140
|
-
import { join as
|
|
1304
|
+
import { join as join8 } from "node:path";
|
|
1141
1305
|
var read2 = (root, rel) => {
|
|
1142
1306
|
try {
|
|
1143
|
-
return readFileSync6(
|
|
1307
|
+
return readFileSync6(join8(root, rel), "utf8");
|
|
1144
1308
|
} catch {
|
|
1145
1309
|
return "";
|
|
1146
1310
|
}
|
|
1147
1311
|
};
|
|
1148
1312
|
var expand2 = (files, p, f = []) => [...new Set((p ?? f).flatMap((x) => matchAny(files, x)))];
|
|
1149
1313
|
var escapeRe2 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1314
|
+
function versionParts(s) {
|
|
1315
|
+
const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(s.trim());
|
|
1316
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
1317
|
+
}
|
|
1318
|
+
function versionCmp(a, b) {
|
|
1319
|
+
return a[0] - b[0] || a[1] - b[1] || a[2] - b[2];
|
|
1320
|
+
}
|
|
1321
|
+
var changelogFreshness = (t, root, files) => {
|
|
1322
|
+
const specs = Array.isArray(t) ? t : [t];
|
|
1323
|
+
const findings = [];
|
|
1324
|
+
let examined = 0;
|
|
1325
|
+
for (const spec of specs) {
|
|
1326
|
+
const src = spec.version ?? {};
|
|
1327
|
+
let current = null;
|
|
1328
|
+
for (const rel of matchAny(files, src.file ?? "package.json")) {
|
|
1329
|
+
try {
|
|
1330
|
+
const raw = (src.path ?? "version").split(".").reduce((o, k) => o?.[k], JSON.parse(read2(root, rel)));
|
|
1331
|
+
current = versionParts(String(raw ?? ""));
|
|
1332
|
+
} catch {
|
|
1333
|
+
}
|
|
1334
|
+
if (current) break;
|
|
1335
|
+
}
|
|
1336
|
+
if (!current) continue;
|
|
1337
|
+
for (const rel of expand2(files, spec.fragments, [])) {
|
|
1338
|
+
const name = rel.split("/").pop().replace(/\.md$/, "");
|
|
1339
|
+
const v = versionParts(name);
|
|
1340
|
+
if (!v) continue;
|
|
1341
|
+
examined++;
|
|
1342
|
+
if (versionCmp(v, current) < 0) {
|
|
1343
|
+
findings.push({
|
|
1344
|
+
file: rel,
|
|
1345
|
+
message: spec.message?.trim() || `fragment names ${name}, below the ${current.join(".")} being prepared \u2014 it was consumed by an earlier release and should have been deleted`
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
return { findings, examined };
|
|
1351
|
+
};
|
|
1150
1352
|
var exempted2 = (text, marker) => !!marker && new RegExp(`${escapeRe2(marker)}\\s*\\S`).test(text);
|
|
1151
1353
|
function tableRows(text, near) {
|
|
1152
1354
|
const lines = text.split("\n");
|
|
@@ -1266,11 +1468,52 @@ var mergeDriverCheck = (t, root) => {
|
|
|
1266
1468
|
}
|
|
1267
1469
|
return { findings, examined: declared.length };
|
|
1268
1470
|
};
|
|
1471
|
+
var boardReconcile = (t, root, _files) => {
|
|
1472
|
+
const rel = t.file;
|
|
1473
|
+
const text = read2(root, rel);
|
|
1474
|
+
if (!text) return { findings: [{ message: `board not found at ${rel}` }], examined: 0 };
|
|
1475
|
+
if (exempted2(text, t.exempt_marker)) return { findings: [], examined: 0 };
|
|
1476
|
+
const groups = t.groups ?? {};
|
|
1477
|
+
const dir = rel.split("/").slice(0, -1).join("/");
|
|
1478
|
+
const findings = [];
|
|
1479
|
+
let heading = "";
|
|
1480
|
+
let examined = 0;
|
|
1481
|
+
for (const line of text.split("\n")) {
|
|
1482
|
+
const h = /^##\s+(.+?)\s*$/.exec(line);
|
|
1483
|
+
if (h) {
|
|
1484
|
+
heading = h[1];
|
|
1485
|
+
continue;
|
|
1486
|
+
}
|
|
1487
|
+
if (!line.startsWith("|")) continue;
|
|
1488
|
+
const link = /^\|\s*\[[^\]]+\]\(([^)]+)\)/.exec(line);
|
|
1489
|
+
if (!link) continue;
|
|
1490
|
+
if (!Object.hasOwn(groups, heading)) continue;
|
|
1491
|
+
examined++;
|
|
1492
|
+
const target = `${dir}/${link[1]}`.replace(/[^/]+\/\.\.\//g, "");
|
|
1493
|
+
const item = read2(root, target);
|
|
1494
|
+
if (!item) {
|
|
1495
|
+
findings.push({ file: rel, message: `row under '${heading}' links to a missing file: ${link[1]}` });
|
|
1496
|
+
continue;
|
|
1497
|
+
}
|
|
1498
|
+
const status = /^status:\s*(\S+)/m.exec(item)?.[1] ?? "";
|
|
1499
|
+
if (!groups[heading].includes(status)) {
|
|
1500
|
+
findings.push({
|
|
1501
|
+
file: rel,
|
|
1502
|
+
message: `${link[1]} is under '${heading}' but its status is '${status}' (expected ${groups[heading].join(" | ")})`
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
const seen = new Set([...text.matchAll(/^##\s+(.+?)\s*$/gm)].map((m) => m[1]));
|
|
1507
|
+
for (const g of Object.keys(groups)) {
|
|
1508
|
+
if (!seen.has(g)) findings.push({ file: rel, message: `declared group '${g}' has no heading in the board` });
|
|
1509
|
+
}
|
|
1510
|
+
return { findings, examined };
|
|
1511
|
+
};
|
|
1269
1512
|
|
|
1270
1513
|
// src/engines.ts
|
|
1271
1514
|
var read3 = (root, rel) => {
|
|
1272
1515
|
try {
|
|
1273
|
-
return readFileSync7(
|
|
1516
|
+
return readFileSync7(join9(root, rel), "utf8");
|
|
1274
1517
|
} catch {
|
|
1275
1518
|
return "";
|
|
1276
1519
|
}
|
|
@@ -1289,10 +1532,12 @@ var fileBudget = (t, root, files) => {
|
|
|
1289
1532
|
const findings = [];
|
|
1290
1533
|
let examined = 0;
|
|
1291
1534
|
for (const rel of targets) {
|
|
1292
|
-
if (excluded.has(rel) || !existsSync6(
|
|
1535
|
+
if (excluded.has(rel) || !existsSync6(join9(root, rel))) continue;
|
|
1293
1536
|
examined++;
|
|
1294
1537
|
const n = loadedLines(read3(root, rel));
|
|
1295
|
-
if (n > t.max_lines)
|
|
1538
|
+
if (n > t.max_lines) {
|
|
1539
|
+
findings.push({ file: rel, message: `${n} loaded lines (blank lines and comments excluded), budget ${t.max_lines}` });
|
|
1540
|
+
}
|
|
1296
1541
|
}
|
|
1297
1542
|
return { findings, examined };
|
|
1298
1543
|
};
|
|
@@ -1304,10 +1549,11 @@ var sections = (t, root, files) => {
|
|
|
1304
1549
|
const targets = dropGenerated(root, spec.file ? [spec.file] : expand3(files, spec.scan));
|
|
1305
1550
|
const excluded = new Set(expand3(files, spec.exclude, []));
|
|
1306
1551
|
for (const rel of targets) {
|
|
1307
|
-
if (excluded.has(rel) || !existsSync6(
|
|
1552
|
+
if (excluded.has(rel) || !existsSync6(join9(root, rel))) continue;
|
|
1308
1553
|
examined++;
|
|
1309
1554
|
const text = read3(root, rel);
|
|
1310
|
-
const
|
|
1555
|
+
const matches = [...text.matchAll(/^(#{1,6})\s+(.+?)\s*$/gm)];
|
|
1556
|
+
const heads = matches.map((m) => m[2]);
|
|
1311
1557
|
for (const want of spec.required ?? []) {
|
|
1312
1558
|
const idx = heads.findIndex((h) => h.toLowerCase().startsWith(String(want).toLowerCase()));
|
|
1313
1559
|
if (idx === -1) {
|
|
@@ -1315,8 +1561,9 @@ var sections = (t, root, files) => {
|
|
|
1315
1561
|
continue;
|
|
1316
1562
|
}
|
|
1317
1563
|
if (spec.non_empty) {
|
|
1318
|
-
const
|
|
1319
|
-
const
|
|
1564
|
+
const level = matches[idx][1].length;
|
|
1565
|
+
const after = text.slice(matches[idx].index + matches[idx][0].length);
|
|
1566
|
+
const body = after.split(new RegExp(`^#{1,${level}}\\s+`, "m"))[0].replace(/<!--[\s\S]*?-->/g, "").trim();
|
|
1320
1567
|
if (!body) findings.push({ file: rel, message: `section '${want}' is empty` });
|
|
1321
1568
|
}
|
|
1322
1569
|
}
|
|
@@ -1346,21 +1593,70 @@ var frontmatterSchema = (t, root, files) => {
|
|
|
1346
1593
|
if (!keys.includes(req)) findings.push({ file: rel, message: `missing '${req}'` });
|
|
1347
1594
|
}
|
|
1348
1595
|
if (spec.allowed) {
|
|
1596
|
+
const optedIn = spec.extensions_allowed_from ? optedInExtensions(rel, spec) : /* @__PURE__ */ new Set();
|
|
1349
1597
|
for (const k of keys) {
|
|
1350
|
-
if (
|
|
1598
|
+
if (spec.allowed.includes(k) || optedIn.has(k)) continue;
|
|
1599
|
+
findings.push({ file: rel, message: `non-spec key '${k}'` });
|
|
1351
1600
|
}
|
|
1352
1601
|
}
|
|
1602
|
+
const field2 = (k) => m[1].match(new RegExp(`^${k}:\\s*(.+)$`, "m"))?.[1].trim().replace(/^["']|["']$/g, "");
|
|
1353
1603
|
for (const [key, values] of Object.entries(spec.enum ?? {})) {
|
|
1354
|
-
const v =
|
|
1604
|
+
const v = field2(key);
|
|
1355
1605
|
if (v && !values.map(String).includes(v)) {
|
|
1356
1606
|
findings.push({ file: rel, message: `${key}='${v}' not one of ${values.join(", ")}` });
|
|
1357
1607
|
}
|
|
1358
1608
|
}
|
|
1609
|
+
for (const pair of spec.reciprocal?.pairs ?? []) {
|
|
1610
|
+
const from = field2(pair.from);
|
|
1611
|
+
if (!from) continue;
|
|
1612
|
+
const target = expand3(files, spec.scan).find((r) => r.includes(from.replace(/\.md$/, "")));
|
|
1613
|
+
if (!target) {
|
|
1614
|
+
findings.push({ file: rel, message: `${pair.from} names '${from}', which is not a record here` });
|
|
1615
|
+
continue;
|
|
1616
|
+
}
|
|
1617
|
+
const back = read3(root, target).match(/^---\n([\s\S]*?)\n---/)?.[1] ?? "";
|
|
1618
|
+
const id = field2("id") ?? "";
|
|
1619
|
+
if (!new RegExp(`^${pair.to}:\\s*.*${escapeRe3(id)}`, "m").test(back)) {
|
|
1620
|
+
findings.push({ file: rel, message: `${pair.from} \u2192 ${from}, but it does not name this record back in '${pair.to}'` });
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
for (const [status, requires] of Object.entries(spec.reciprocal?.required_when ?? {})) {
|
|
1624
|
+
if (field2("status") === status && !field2(String(requires))) {
|
|
1625
|
+
findings.push({ file: rel, message: `status is '${status}' but '${requires}' is absent` });
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1359
1628
|
}
|
|
1360
1629
|
}
|
|
1361
1630
|
return { findings, examined };
|
|
1362
1631
|
};
|
|
1632
|
+
function resolvesHere(root, rel, href) {
|
|
1633
|
+
const from = dirname4(rel);
|
|
1634
|
+
const decoded = decodeURIComponent(href);
|
|
1635
|
+
if (existsSync6(resolve2(root, from, decoded))) return true;
|
|
1636
|
+
const stripped = decoded.replace(/:\d+(?::\d+)?$/, "");
|
|
1637
|
+
return stripped !== decoded && existsSync6(resolve2(root, from, stripped));
|
|
1638
|
+
}
|
|
1639
|
+
function backtickedPaths(rel, text, root, hints) {
|
|
1640
|
+
const out = [];
|
|
1641
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1642
|
+
for (const m of text.matchAll(/`([^`\n]+)`/g)) {
|
|
1643
|
+
const raw = m[1].trim();
|
|
1644
|
+
if (seen.has(raw) || !hints.some((h) => raw.includes(h))) continue;
|
|
1645
|
+
seen.add(raw);
|
|
1646
|
+
if (raw.startsWith("/")) continue;
|
|
1647
|
+
if (/[*?{}#\s]|^\w+:/.test(raw)) continue;
|
|
1648
|
+
if (!raw.includes("/")) continue;
|
|
1649
|
+
if (!/\.[a-z0-9]{1,5}$/i.test(raw)) continue;
|
|
1650
|
+
const bare = raw.replace(/^\.\//, "");
|
|
1651
|
+
if (!existsSync6(join9(root, bare)) && !existsSync6(resolve2(root, dirname4(rel), bare))) {
|
|
1652
|
+
out.push({ message: `stale path in a code span \u2192 ${raw}` });
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
return out;
|
|
1656
|
+
}
|
|
1363
1657
|
var linkIntegrity = (t, root, files) => {
|
|
1658
|
+
if (Array.isArray(t)) t = t[0] ?? {};
|
|
1659
|
+
const checks = t.check ?? ["relative_markdown_links"];
|
|
1364
1660
|
const scan = expand3(files, t.scan, ["**/*.md"]);
|
|
1365
1661
|
const excluded = new Set(expand3(files, t.exclude, []));
|
|
1366
1662
|
const findings = [];
|
|
@@ -1370,11 +1666,14 @@ var linkIntegrity = (t, root, files) => {
|
|
|
1370
1666
|
const text = read3(root, rel);
|
|
1371
1667
|
examined++;
|
|
1372
1668
|
if (/path-ok:\s*\S/.test(text)) continue;
|
|
1669
|
+
if (checks.includes("backticked_paths")) {
|
|
1670
|
+
findings.push(...backtickedPaths(rel, text, root, t.path_hint ?? ["/"]).map((f) => ({ ...f, file: rel })));
|
|
1671
|
+
}
|
|
1672
|
+
if (!checks.includes("relative_markdown_links")) continue;
|
|
1373
1673
|
const scannable = text.replace(/`+[^`\n]*`+/g, (s) => " ".repeat(s.length));
|
|
1374
1674
|
for (const m of scannable.matchAll(/\]\((?!https?:|#|mailto:)([^)\s#]+)/g)) {
|
|
1375
1675
|
if (/\{\{[a-z_.]+\}\}/.test(m[1])) continue;
|
|
1376
|
-
|
|
1377
|
-
if (!existsSync6(target)) findings.push({ file: rel, message: `broken link \u2192 ${m[1]}` });
|
|
1676
|
+
if (!resolvesHere(root, rel, m[1])) findings.push({ file: rel, message: `broken link \u2192 ${m[1]}` });
|
|
1378
1677
|
}
|
|
1379
1678
|
}
|
|
1380
1679
|
return { findings, examined };
|
|
@@ -1392,13 +1691,17 @@ var filePopulation = (t, root, files) => {
|
|
|
1392
1691
|
const findings = [];
|
|
1393
1692
|
const failAt = t.fail_at ?? Infinity;
|
|
1394
1693
|
if (hits.length >= failAt) {
|
|
1395
|
-
|
|
1694
|
+
const scanned = [t.scan ?? []].flat();
|
|
1695
|
+
findings.push({
|
|
1696
|
+
message: `${hits.length} matching file(s), threshold ${failAt} (e.g. ${hits[0]})` + (scanned.length ? ` \u2014 matched against ${scanned.join(", ")}` : "")
|
|
1697
|
+
});
|
|
1396
1698
|
}
|
|
1397
1699
|
return { findings, examined: hits.length };
|
|
1398
1700
|
};
|
|
1399
1701
|
var gateMeta = (_t, root) => {
|
|
1400
1702
|
const findings = [];
|
|
1401
|
-
|
|
1703
|
+
let unrun = 0;
|
|
1704
|
+
const registry = join9(root, ".ai", "gates.toml");
|
|
1402
1705
|
if (!existsSync6(registry)) return { findings, examined: 0 };
|
|
1403
1706
|
const text = readFileSync7(registry, "utf8");
|
|
1404
1707
|
const entries = [...text.matchAll(/\[\[gates\]\][\s\S]*?(?=\n\[\[gates\]\]|\n# rungs:end|$)/g)].map((m) => m[0]);
|
|
@@ -1409,7 +1712,7 @@ var gateMeta = (_t, root) => {
|
|
|
1409
1712
|
const table = entry.match(/^table\s*=\s*"(.+)"/m)?.[1];
|
|
1410
1713
|
if (!id || kind !== "declared" || !table) continue;
|
|
1411
1714
|
examined++;
|
|
1412
|
-
const tablePath =
|
|
1715
|
+
const tablePath = join9(dirname4(new URL(import.meta.url).pathname.slice(1)), "..", "modules", dirname4(table), "gates", table.split("/").pop());
|
|
1413
1716
|
const src = existsSync6(tablePath) ? readFileSync7(tablePath, "utf8") : "";
|
|
1414
1717
|
const forGate = [...src.matchAll(/\[\[self_test\]\][\s\S]*?(?=\n\[\[|\n\[|$)/g)].map((m) => m[0]).filter((b) => b.includes(`gate = "${id}"`) || b.includes(`gate = "${id}"`) || b.includes(`gate = "${id}"`));
|
|
1415
1718
|
for (const direction of ["pass", "fail"]) {
|
|
@@ -1417,10 +1720,61 @@ var gateMeta = (_t, root) => {
|
|
|
1417
1720
|
findings.push({ message: `gate '${id}' has no self-test expecting '${direction}'` });
|
|
1418
1721
|
}
|
|
1419
1722
|
}
|
|
1723
|
+
const engine = entry.match(/^engine\s*=\s*"(.+)"/m)?.[1];
|
|
1724
|
+
const parsed = parseTable(tablePath, table.split("/")[0]);
|
|
1725
|
+
if (engine && parsed) {
|
|
1726
|
+
const blocks = (Array.isArray(parsed.self_test) ? parsed.self_test : []).filter((b) => b?.gate === id).map((b) => ({ expect: String(b.expect), input: b.input, fixture: b.fixture }));
|
|
1727
|
+
for (const r of runSelfTests(id, engine, parsed[tableKeyFor(engine)] ?? parsed, blocks)) {
|
|
1728
|
+
if (r.outcome === "mismatch") findings.push({ message: `self-test for '${id}' ${r.detail}` });
|
|
1729
|
+
else if (r.outcome === "unrun") unrun++;
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1420
1732
|
}
|
|
1733
|
+
if (unrun) console.error(` ${unrun} self-test fixture(s) have no builder and did not run \u2014 not passes (F-018)`);
|
|
1421
1734
|
return { findings, examined };
|
|
1422
1735
|
};
|
|
1736
|
+
function optedInExtensions(rel, spec) {
|
|
1737
|
+
if (Array.isArray(spec.extensions_opted_in)) return new Set(spec.extensions_opted_in.map(String));
|
|
1738
|
+
const name = rel.split("/").slice(-2)[0];
|
|
1739
|
+
if (!name) return /* @__PURE__ */ new Set();
|
|
1740
|
+
try {
|
|
1741
|
+
const mods = loadAllModules(join9(dirname4(new URL(import.meta.url).pathname.slice(1)), "..", "modules"));
|
|
1742
|
+
const owner = mods.find((m) => m.skills?.[name]?.extensions);
|
|
1743
|
+
return new Set(Object.keys(owner?.skills?.[name]?.extensions ?? {}));
|
|
1744
|
+
} catch {
|
|
1745
|
+
return /* @__PURE__ */ new Set();
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1423
1748
|
var escapeRe3 = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1749
|
+
function parseTable(path, module) {
|
|
1750
|
+
if (!existsSync6(path)) return null;
|
|
1751
|
+
try {
|
|
1752
|
+
const mods = loadAllModules(join9(dirname4(new URL(import.meta.url).pathname.slice(1)), "..", "modules"));
|
|
1753
|
+
const params = resolveParams(mods, {}, ".");
|
|
1754
|
+
return parseToml(substitute(readFileSync7(path, "utf8"), module, params));
|
|
1755
|
+
} catch {
|
|
1756
|
+
return null;
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
var tableKeyFor = (engine) => ({
|
|
1760
|
+
"file-budget": "file_budget",
|
|
1761
|
+
"frontmatter-schema": "frontmatter_schema",
|
|
1762
|
+
"link-integrity": "link_integrity",
|
|
1763
|
+
"file-population": "file_population",
|
|
1764
|
+
"render-freshness": "render_freshness",
|
|
1765
|
+
"register-schema": "register_schema",
|
|
1766
|
+
"self-declared-closure": "self_declared_closure",
|
|
1767
|
+
"filename-schema": "filename_schema",
|
|
1768
|
+
"cross-reference": "cross_reference",
|
|
1769
|
+
"git-status-reconcile": "merged_status",
|
|
1770
|
+
"computed-claim": "computed_claim",
|
|
1771
|
+
"term-ownership": "term_ownership",
|
|
1772
|
+
"rule-propagation": "rule_propagation",
|
|
1773
|
+
"git-state": "git_state",
|
|
1774
|
+
"merge-driver-check": "merge_driver_check",
|
|
1775
|
+
"board-reconcile": "board_reconcile",
|
|
1776
|
+
"changelog-freshness": "changelog_freshness"
|
|
1777
|
+
})[engine] ?? engine;
|
|
1424
1778
|
var ENGINES = {
|
|
1425
1779
|
"file-budget": fileBudget,
|
|
1426
1780
|
sections,
|
|
@@ -1439,27 +1793,49 @@ var ENGINES = {
|
|
|
1439
1793
|
"term-ownership": termOwnership,
|
|
1440
1794
|
"rule-propagation": rulePropagation,
|
|
1441
1795
|
"git-state": gitState,
|
|
1442
|
-
"merge-driver-check": mergeDriverCheck
|
|
1796
|
+
"merge-driver-check": mergeDriverCheck,
|
|
1797
|
+
"board-reconcile": boardReconcile,
|
|
1798
|
+
"changelog-freshness": changelogFreshness
|
|
1443
1799
|
};
|
|
1444
1800
|
function isImplemented(engine) {
|
|
1445
1801
|
return engine in ENGINES;
|
|
1446
1802
|
}
|
|
1447
1803
|
|
|
1448
1804
|
// src/check.ts
|
|
1449
|
-
var MODULES =
|
|
1805
|
+
var MODULES = join10(dirname5(fileURLToPath(import.meta.url)), "..", "modules");
|
|
1450
1806
|
function loadRegistry(repoRoot) {
|
|
1451
|
-
const path =
|
|
1807
|
+
const path = join10(repoRoot, ".ai", "gates.toml");
|
|
1452
1808
|
if (!existsSync7(path)) return { runner: {}, gates: [] };
|
|
1453
1809
|
const raw = parse2(readFileSync8(path, "utf8"));
|
|
1454
1810
|
return { runner: raw.runner ?? {}, gates: raw.gates ?? [] };
|
|
1455
1811
|
}
|
|
1812
|
+
function tierSelects(runnerTiers, requested, gateTier) {
|
|
1813
|
+
if (!gateTier) return true;
|
|
1814
|
+
const at = runnerTiers.indexOf(requested);
|
|
1815
|
+
const of = runnerTiers.indexOf(gateTier);
|
|
1816
|
+
if (at < 0 || of < 0) return gateTier === requested;
|
|
1817
|
+
return of <= at;
|
|
1818
|
+
}
|
|
1819
|
+
var UnknownTierError = class extends Error {
|
|
1820
|
+
requested;
|
|
1821
|
+
declared;
|
|
1822
|
+
constructor(requested, declared) {
|
|
1823
|
+
super(`unknown tier "${requested}"`);
|
|
1824
|
+
this.requested = requested;
|
|
1825
|
+
this.declared = declared;
|
|
1826
|
+
}
|
|
1827
|
+
};
|
|
1456
1828
|
function runGates(repoRoot, tier, now = () => Date.now()) {
|
|
1457
|
-
const { gates } = loadRegistry(repoRoot);
|
|
1829
|
+
const { runner, gates } = loadRegistry(repoRoot);
|
|
1830
|
+
const runnerTiers = Array.isArray(runner?.tiers) ? runner.tiers : [];
|
|
1831
|
+
if (tier && runnerTiers.length && !runnerTiers.includes(tier)) {
|
|
1832
|
+
throw new UnknownTierError(tier, runnerTiers);
|
|
1833
|
+
}
|
|
1458
1834
|
const files = walk(repoRoot);
|
|
1459
1835
|
const runs = [];
|
|
1460
1836
|
for (const g of gates) {
|
|
1461
1837
|
if (g.trigger) continue;
|
|
1462
|
-
if (tier &&
|
|
1838
|
+
if (tier && !tierSelects(runnerTiers, tier, g.tier)) continue;
|
|
1463
1839
|
const started = now();
|
|
1464
1840
|
let status = "pass";
|
|
1465
1841
|
let findings = [];
|
|
@@ -1515,7 +1891,7 @@ function runGates(repoRoot, tier, now = () => Date.now()) {
|
|
|
1515
1891
|
function loadTable(ref, repoRoot) {
|
|
1516
1892
|
if (!ref) return null;
|
|
1517
1893
|
const [mod, file] = ref.split("/");
|
|
1518
|
-
const path =
|
|
1894
|
+
const path = join10(MODULES, mod, "gates", file);
|
|
1519
1895
|
if (!existsSync7(path)) return null;
|
|
1520
1896
|
try {
|
|
1521
1897
|
return parse2(substitute(readFileSync8(path, "utf8"), mod, installedParams(repoRoot)));
|
|
@@ -1527,7 +1903,7 @@ var paramCache = null;
|
|
|
1527
1903
|
function installedParams(repoRoot) {
|
|
1528
1904
|
if (paramCache?.root === repoRoot) return paramCache.params;
|
|
1529
1905
|
const defaults = resolveParams(loadAllModules(MODULES), {}, repoRoot);
|
|
1530
|
-
const recordPath =
|
|
1906
|
+
const recordPath = join10(repoRoot, ".ai", "rungs.toml");
|
|
1531
1907
|
if (existsSync7(recordPath)) {
|
|
1532
1908
|
try {
|
|
1533
1909
|
const rec = parse2(readFileSync8(recordPath, "utf8"));
|
|
@@ -1558,17 +1934,18 @@ var tableKey = (engine) => ({
|
|
|
1558
1934
|
"term-ownership": "term_ownership",
|
|
1559
1935
|
"rule-propagation": "rule_propagation",
|
|
1560
1936
|
"git-state": "git_state",
|
|
1561
|
-
"merge-driver-check": "merge_driver_check"
|
|
1937
|
+
"merge-driver-check": "merge_driver_check",
|
|
1938
|
+
"board-reconcile": "board_reconcile"
|
|
1562
1939
|
})[engine] ?? engine;
|
|
1563
1940
|
function appendLedger(repoRoot, runs, stamp) {
|
|
1564
1941
|
const { runner } = loadRegistry(repoRoot);
|
|
1565
1942
|
if (runner.ledger === false) return;
|
|
1566
|
-
const path =
|
|
1943
|
+
const path = join10(repoRoot, ".ai", ".gate-ledger.jsonl");
|
|
1567
1944
|
const lines = runs.map((r) => JSON.stringify({ at: stamp, id: r.id, status: r.status, ms: r.ms, examined: r.examined })).join("\n");
|
|
1568
1945
|
appendFileSync(path, lines + "\n");
|
|
1569
1946
|
}
|
|
1570
1947
|
function ledgerQuestions(repoRoot, gates) {
|
|
1571
|
-
const path =
|
|
1948
|
+
const path = join10(repoRoot, ".ai", ".gate-ledger.jsonl");
|
|
1572
1949
|
if (!existsSync7(path)) return { neverFired: [], alwaysFires: [], runs: 0 };
|
|
1573
1950
|
const rows = readFileSync8(path, "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l));
|
|
1574
1951
|
const by = /* @__PURE__ */ new Map();
|
|
@@ -1585,12 +1962,12 @@ function ledgerQuestions(repoRoot, gates) {
|
|
|
1585
1962
|
}
|
|
1586
1963
|
|
|
1587
1964
|
// src/lifecycle.ts
|
|
1588
|
-
import { copyFileSync, existsSync as existsSync8, mkdirSync as
|
|
1589
|
-
import { dirname as
|
|
1965
|
+
import { copyFileSync, existsSync as existsSync8, mkdirSync as mkdirSync4, readFileSync as readFileSync9, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1966
|
+
import { dirname as dirname6, join as join11 } from "node:path";
|
|
1590
1967
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1591
1968
|
import { execSync as execSync4 } from "node:child_process";
|
|
1592
1969
|
import { parse as parse3 } from "smol-toml";
|
|
1593
|
-
var SRC =
|
|
1970
|
+
var SRC = dirname6(fileURLToPath2(import.meta.url));
|
|
1594
1971
|
var PROFILES = {
|
|
1595
1972
|
minimal: ["instructions"],
|
|
1596
1973
|
tracked: ["instructions", "gates", "backlog", "findings", "adr", "session"],
|
|
@@ -1599,7 +1976,7 @@ var PROFILES = {
|
|
|
1599
1976
|
fleet: ["instructions", "gates", "backlog", "findings", "adr", "session", "ci", "specs", "workflows", "skills", "audit", "release", "doc-authority", "concurrency", "design-sync"]
|
|
1600
1977
|
};
|
|
1601
1978
|
function readRecord(repoRoot) {
|
|
1602
|
-
const p =
|
|
1979
|
+
const p = join11(repoRoot, ".ai", "rungs.toml");
|
|
1603
1980
|
if (!existsSync8(p)) return null;
|
|
1604
1981
|
try {
|
|
1605
1982
|
const raw = parse3(readFileSync9(p, "utf8"));
|
|
@@ -1620,7 +1997,7 @@ function planUpgrade(repoRoot, mods, record) {
|
|
|
1620
1997
|
const kept = new Set(installed.kept?.files ?? []);
|
|
1621
1998
|
for (const [rel, wouldEmit] of emitted) {
|
|
1622
1999
|
if (kept.has(rel)) continue;
|
|
1623
|
-
const full =
|
|
2000
|
+
const full = join11(repoRoot, rel);
|
|
1624
2001
|
if (!existsSync8(full)) {
|
|
1625
2002
|
files.push({ rel, state: "missing" });
|
|
1626
2003
|
continue;
|
|
@@ -1639,18 +2016,63 @@ function applyUpgrade(repoRoot, mods, record, plan) {
|
|
|
1639
2016
|
const params = resolveParams(mods, paramsFrom(record), repoRoot);
|
|
1640
2017
|
const skillsDir = record.harnesses.includes("claude") ? ".claude/skills" : ".agents/skills";
|
|
1641
2018
|
let written = 0;
|
|
2019
|
+
const rewritten = /* @__PURE__ */ new Map();
|
|
1642
2020
|
for (const item of plan) {
|
|
1643
2021
|
const mod = mods.find((m) => m.name === item.module);
|
|
1644
2022
|
const emitted = emittedFiles(mod, params, skillsDir);
|
|
1645
2023
|
for (const f of item.files) {
|
|
1646
2024
|
if (f.state !== "stale" && f.state !== "missing") continue;
|
|
1647
|
-
const full =
|
|
1648
|
-
|
|
1649
|
-
|
|
2025
|
+
const full = join11(repoRoot, f.rel);
|
|
2026
|
+
const content = emitted.get(f.rel);
|
|
2027
|
+
mkdirSync4(dirname6(full), { recursive: true });
|
|
2028
|
+
writeFileSync4(full, content);
|
|
2029
|
+
if (!rewritten.has(mod.name)) rewritten.set(mod.name, /* @__PURE__ */ new Map());
|
|
2030
|
+
rewritten.get(mod.name).set(f.rel, contentHash(content));
|
|
1650
2031
|
written++;
|
|
1651
2032
|
}
|
|
1652
2033
|
}
|
|
1653
|
-
|
|
2034
|
+
const upgraded = plan.map((p) => mods.find((m) => m.name === p.module)).filter(Boolean);
|
|
2035
|
+
const gateActions = upgraded.length ? registerGates(upgraded, repoRoot, false) : [];
|
|
2036
|
+
const recorded = updateRecordAfterUpgrade(
|
|
2037
|
+
repoRoot,
|
|
2038
|
+
upgraded.map((m) => ({ module: m.name, version: m.version, hashes: rewritten.get(m.name) ?? /* @__PURE__ */ new Map() }))
|
|
2039
|
+
);
|
|
2040
|
+
return { written, gates: gateActions.length, recorded };
|
|
2041
|
+
}
|
|
2042
|
+
function updateRecordAfterUpgrade(repoRoot, updates) {
|
|
2043
|
+
const path = join11(repoRoot, ".ai", "rungs.toml");
|
|
2044
|
+
if (!existsSync8(path) || !updates.length) return 0;
|
|
2045
|
+
const lines = readFileSync9(path, "utf8").split("\n");
|
|
2046
|
+
const byModule = new Map(updates.map((u) => [u.module, u]));
|
|
2047
|
+
let changed = 0;
|
|
2048
|
+
let current = null;
|
|
2049
|
+
const out = [];
|
|
2050
|
+
for (const line of lines) {
|
|
2051
|
+
const header = /^\[modules\.([^\].]+)(\.[^\]]+)?\]/.exec(line);
|
|
2052
|
+
if (header) {
|
|
2053
|
+
current = byModule.has(header[1]) ? { module: header[1], hashes: header[2] === ".hashes" } : null;
|
|
2054
|
+
out.push(line);
|
|
2055
|
+
continue;
|
|
2056
|
+
}
|
|
2057
|
+
if (current && !current.hashes && /^version\s*=/.test(line)) {
|
|
2058
|
+
const next = `version = "${byModule.get(current.module).version}"`;
|
|
2059
|
+
if (next !== line) changed++;
|
|
2060
|
+
out.push(next);
|
|
2061
|
+
continue;
|
|
2062
|
+
}
|
|
2063
|
+
if (current?.hashes) {
|
|
2064
|
+
const entry = /^"([^"]+)"\s*=/.exec(line);
|
|
2065
|
+
const replacement = entry && byModule.get(current.module).hashes.get(entry[1]);
|
|
2066
|
+
if (replacement) {
|
|
2067
|
+
out.push(`"${entry[1]}" = "${replacement}"`);
|
|
2068
|
+
changed++;
|
|
2069
|
+
continue;
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
out.push(line);
|
|
2073
|
+
}
|
|
2074
|
+
writeFileSync4(path, out.join("\n"));
|
|
2075
|
+
return changed;
|
|
1654
2076
|
}
|
|
1655
2077
|
function paramsFrom(record) {
|
|
1656
2078
|
const out = {};
|
|
@@ -1660,7 +2082,7 @@ function paramsFrom(record) {
|
|
|
1660
2082
|
return out;
|
|
1661
2083
|
}
|
|
1662
2084
|
function eject(repoRoot, mods, dryRun = false) {
|
|
1663
|
-
const dest =
|
|
2085
|
+
const dest = join11(repoRoot, ".rungs");
|
|
1664
2086
|
const engines = ["glob.ts", "engines.ts", "engines2.ts"];
|
|
1665
2087
|
const { gates } = loadRegistry(repoRoot);
|
|
1666
2088
|
const declared = gates.filter((g) => g.kind === "declared" && g.table);
|
|
@@ -1670,23 +2092,23 @@ function eject(repoRoot, mods, dryRun = false) {
|
|
|
1670
2092
|
for (const t of tables) actions.push(`.rungs/tables/${t.replace("/", "-").replace(/.toml$/, ".json")}`);
|
|
1671
2093
|
actions.push(".rungs/run-gate.mjs", ".ai/gates.toml (rewritten to command gates)");
|
|
1672
2094
|
if (dryRun) return { actions, gates: declared.length };
|
|
1673
|
-
|
|
1674
|
-
for (const f of engines) copyFileSync(
|
|
2095
|
+
mkdirSync4(join11(dest, "tables"), { recursive: true });
|
|
2096
|
+
for (const f of engines) copyFileSync(join11(SRC, f), join11(dest, f));
|
|
1675
2097
|
const record = readRecord(repoRoot);
|
|
1676
2098
|
const params = resolveParams(mods, record ? paramsFrom(record) : {}, repoRoot);
|
|
1677
2099
|
for (const t of tables) {
|
|
1678
2100
|
const [mod, file] = t.split("/");
|
|
1679
|
-
const src =
|
|
2101
|
+
const src = join11(SRC, "..", "modules", mod, "gates", file);
|
|
1680
2102
|
if (!existsSync8(src)) continue;
|
|
1681
2103
|
try {
|
|
1682
2104
|
const parsed = parse3(substitute(readFileSync9(src, "utf8"), mod, params));
|
|
1683
|
-
|
|
2105
|
+
writeFileSync4(join11(dest, "tables", `${mod}-${file.replace(/\.toml$/, ".json")}`), JSON.stringify(parsed, null, 2));
|
|
1684
2106
|
} catch {
|
|
1685
2107
|
}
|
|
1686
2108
|
}
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
const registry =
|
|
2109
|
+
writeFileSync4(join11(dest, "run-gate.mjs"), RUNNER);
|
|
2110
|
+
writeFileSync4(join11(dest, "README.md"), EJECT_README);
|
|
2111
|
+
const registry = join11(repoRoot, ".ai", "gates.toml");
|
|
1690
2112
|
let text = readFileSync9(registry, "utf8");
|
|
1691
2113
|
for (const g of declared) {
|
|
1692
2114
|
text = text.replace(
|
|
@@ -1695,7 +2117,7 @@ function eject(repoRoot, mods, dryRun = false) {
|
|
|
1695
2117
|
command = "node .rungs/run-gate.mjs ${g.id}"`
|
|
1696
2118
|
);
|
|
1697
2119
|
}
|
|
1698
|
-
|
|
2120
|
+
writeFileSync4(registry, `${text}
|
|
1699
2121
|
# Ejected: gates above run from .rungs/ and no longer need rungs installed.
|
|
1700
2122
|
`);
|
|
1701
2123
|
return { actions, gates: declared.length };
|
|
@@ -1749,7 +2171,7 @@ itself to whoever finds it.
|
|
|
1749
2171
|
To go back, delete this directory and re-run \`rungs add\`.
|
|
1750
2172
|
`;
|
|
1751
2173
|
function setupGit(repoRoot, dryRun = false) {
|
|
1752
|
-
const attrs =
|
|
2174
|
+
const attrs = join11(repoRoot, ".gitattributes");
|
|
1753
2175
|
if (!existsSync8(attrs)) return { drivers: [], rerere: false };
|
|
1754
2176
|
const drivers = [...new Set([...readFileSync9(attrs, "utf8").matchAll(/merge=(rungs-[\w-]+)/g)].map((m) => m[1]))];
|
|
1755
2177
|
const done = [];
|
|
@@ -1777,9 +2199,173 @@ function setupGit(repoRoot, dryRun = false) {
|
|
|
1777
2199
|
return { drivers: done, rerere };
|
|
1778
2200
|
}
|
|
1779
2201
|
|
|
2202
|
+
// src/explain.ts
|
|
2203
|
+
var isRunnable = (g) => g.kind !== "command" && !g.trigger && !!g.engine;
|
|
2204
|
+
var IN_SCOPE = /* @__PURE__ */ new Set(["theirs", "ours-current", "ours-diverged"]);
|
|
2205
|
+
var FOREIGN_SAFE = /* @__PURE__ */ new Set(["repo-content"]);
|
|
2206
|
+
function explain(mods, results, repoRoot, files) {
|
|
2207
|
+
return explainWith(ENGINES, mods, results, repoRoot, files);
|
|
2208
|
+
}
|
|
2209
|
+
function explainWith(engines, mods, results, repoRoot, files) {
|
|
2210
|
+
const inScope = results.filter((r) => IN_SCOPE.has(r.state));
|
|
2211
|
+
const scope = inScope.map((r) => r.module);
|
|
2212
|
+
const stateOf = new Map(inScope.map((r) => [r.module, r.state]));
|
|
2213
|
+
const reported = [];
|
|
2214
|
+
const skipped = { command: 0, unimplemented: [], undeclared: [], errored: [] };
|
|
2215
|
+
for (const name of scope) {
|
|
2216
|
+
const mod = mods.find((m) => m.name === name);
|
|
2217
|
+
if (!mod) continue;
|
|
2218
|
+
const isOurs = stateOf.get(name) !== "theirs";
|
|
2219
|
+
for (const g of mod.gates) {
|
|
2220
|
+
if (!isRunnable(g)) {
|
|
2221
|
+
if (g.kind === "command") skipped.command++;
|
|
2222
|
+
continue;
|
|
2223
|
+
}
|
|
2224
|
+
if (!isOurs) {
|
|
2225
|
+
if (!g.applicability) {
|
|
2226
|
+
skipped.undeclared.push(g.id);
|
|
2227
|
+
continue;
|
|
2228
|
+
}
|
|
2229
|
+
if (!FOREIGN_SAFE.has(g.applicability)) continue;
|
|
2230
|
+
}
|
|
2231
|
+
if (!(g.engine in engines)) {
|
|
2232
|
+
skipped.unimplemented.push(g.id);
|
|
2233
|
+
continue;
|
|
2234
|
+
}
|
|
2235
|
+
const table = loadTable(g.table ? `${mod.name}/${g.table.replace(/^gates\//, "")}` : void 0, repoRoot);
|
|
2236
|
+
if (!table) {
|
|
2237
|
+
skipped.errored.push({ gate: g.id, message: `table '${g.table ?? "(none)"}' not found` });
|
|
2238
|
+
continue;
|
|
2239
|
+
}
|
|
2240
|
+
try {
|
|
2241
|
+
const key = tableKey(g.engine);
|
|
2242
|
+
let section = table[key] ?? table;
|
|
2243
|
+
if (Array.isArray(section) && section.some((s) => s?.id)) {
|
|
2244
|
+
const mine = section.filter((s) => !s.id || g.id.includes(s.id));
|
|
2245
|
+
if (mine.length) section = mine;
|
|
2246
|
+
}
|
|
2247
|
+
const r = engines[g.engine](section, repoRoot, files);
|
|
2248
|
+
if (r.findings.length) {
|
|
2249
|
+
reported.push({ module: mod.name, gate: g.id, why: g.why, findings: r.findings, examined: r.examined });
|
|
2250
|
+
}
|
|
2251
|
+
} catch (e) {
|
|
2252
|
+
skipped.errored.push({ gate: g.id, message: e.message });
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
return { reported: collapseDuplicates(reported), skipped, scope };
|
|
2257
|
+
}
|
|
2258
|
+
function collapseDuplicates(reported) {
|
|
2259
|
+
const out = [];
|
|
2260
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2261
|
+
for (const r of reported) {
|
|
2262
|
+
const key = `${r.module} ${r.findings.map((f) => `${f.file ?? ""}|${f.message}`).join("")}`;
|
|
2263
|
+
const prior = seen.get(key);
|
|
2264
|
+
if (prior) {
|
|
2265
|
+
prior.gate = `${prior.gate} + ${r.gate}`;
|
|
2266
|
+
continue;
|
|
2267
|
+
}
|
|
2268
|
+
seen.set(key, r);
|
|
2269
|
+
out.push(r);
|
|
2270
|
+
}
|
|
2271
|
+
return out;
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
// src/backlog.ts
|
|
2275
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync10, renameSync, writeFileSync as writeFileSync5 } from "node:fs";
|
|
2276
|
+
import { dirname as dirname7, join as join12, relative as relative2, resolve as resolve3, sep as sep2 } from "node:path";
|
|
2277
|
+
var FINISHED = /* @__PURE__ */ new Set(["done", "rejected"]);
|
|
2278
|
+
var field = (text, name) => text.match(new RegExp(`^${name}:\\s*(\\S+)`, "m"))?.[1] ?? "";
|
|
2279
|
+
var posix = (p) => p.split(sep2).join("/");
|
|
2280
|
+
var LINK = /\]\((?!https?:|#|mailto:)([^)\s#]+)((?:#[^)\s]*)?)\)/g;
|
|
2281
|
+
function planArchive(repoRoot, backlogRoot = "docs/backlog") {
|
|
2282
|
+
const itemsDir = join12(repoRoot, ...backlogRoot.split("/"), "items");
|
|
2283
|
+
const archiveDir = join12(repoRoot, ...backlogRoot.split("/"), "archive");
|
|
2284
|
+
const moves = [];
|
|
2285
|
+
const held = [];
|
|
2286
|
+
const files = walk(repoRoot);
|
|
2287
|
+
const items = files.filter((f) => posix(f).startsWith(posix(relative2(repoRoot, itemsDir)) + "/") && f.endsWith(".md"));
|
|
2288
|
+
for (const rel of items) {
|
|
2289
|
+
if (/README\.md$/i.test(rel) || /TEMPLATE\.md$/i.test(rel)) continue;
|
|
2290
|
+
const text = readFileSync10(join12(repoRoot, rel), "utf8");
|
|
2291
|
+
const status = field(text, "status");
|
|
2292
|
+
const id = field(text, "id");
|
|
2293
|
+
if (!FINISHED.has(status)) continue;
|
|
2294
|
+
if (field(text, "type") === "epic") {
|
|
2295
|
+
const children = (text.match(/^children:\s*\[(.*)\]/m)?.[1] ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
2296
|
+
const unfinished = children.filter((c2) => {
|
|
2297
|
+
const f = items.find((i) => i.includes(`${c2}-`));
|
|
2298
|
+
return !f || !FINISHED.has(field(readFileSync10(join12(repoRoot, f), "utf8"), "status"));
|
|
2299
|
+
});
|
|
2300
|
+
if (unfinished.length) {
|
|
2301
|
+
held.push({ file: rel, reason: `epic with unfinished children: ${unfinished.join(", ")}` });
|
|
2302
|
+
continue;
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
moves.push({
|
|
2306
|
+
id,
|
|
2307
|
+
status,
|
|
2308
|
+
from: rel,
|
|
2309
|
+
to: posix(join12(relative2(repoRoot, archiveDir), posix(rel).split("/").pop()))
|
|
2310
|
+
});
|
|
2311
|
+
}
|
|
2312
|
+
const moved = new Map(moves.map((m) => [resolve3(repoRoot, m.from), m.to]));
|
|
2313
|
+
const rewrites = [];
|
|
2314
|
+
for (const rel of files) {
|
|
2315
|
+
if (!isRewritable(rel)) continue;
|
|
2316
|
+
const links = retargets(repoRoot, rel, moved).length;
|
|
2317
|
+
if (links || moved.has(resolve3(repoRoot, rel))) rewrites.push({ file: rel, links });
|
|
2318
|
+
}
|
|
2319
|
+
return { root: backlogRoot, moves, rewrites, held };
|
|
2320
|
+
}
|
|
2321
|
+
function isRewritable(rel) {
|
|
2322
|
+
const p = posix(rel);
|
|
2323
|
+
if (!p.endsWith(".md")) return false;
|
|
2324
|
+
return !/^modules\/[^/]+\/(files|fragments)\//.test(p) && !p.startsWith("node_modules/");
|
|
2325
|
+
}
|
|
2326
|
+
function retargets(repoRoot, rel, moved) {
|
|
2327
|
+
const oldDir = dirname7(resolve3(repoRoot, rel));
|
|
2328
|
+
const selfMoved = moved.get(resolve3(repoRoot, rel));
|
|
2329
|
+
const newDir = dirname7(resolve3(repoRoot, selfMoved ?? rel));
|
|
2330
|
+
const out = [];
|
|
2331
|
+
for (const m of readFileSync10(join12(repoRoot, rel), "utf8").matchAll(LINK)) {
|
|
2332
|
+
const href = m[1];
|
|
2333
|
+
if (href.includes("{{")) continue;
|
|
2334
|
+
const target = resolve3(oldDir, decodeURIComponent(href));
|
|
2335
|
+
const targetMoved = moved.get(target);
|
|
2336
|
+
if (!targetMoved && !selfMoved) continue;
|
|
2337
|
+
if (!targetMoved && !existsSync9(target)) continue;
|
|
2338
|
+
const targetNew = targetMoved ? resolve3(repoRoot, targetMoved) : target;
|
|
2339
|
+
const to = posix(relative2(newDir, targetNew));
|
|
2340
|
+
if (to !== posix(href)) out.push({ href, to });
|
|
2341
|
+
}
|
|
2342
|
+
return out;
|
|
2343
|
+
}
|
|
2344
|
+
function applyArchive(repoRoot, plan) {
|
|
2345
|
+
const moved = new Map(plan.moves.map((m) => [resolve3(repoRoot, m.from), m.to]));
|
|
2346
|
+
for (const rel of walk(repoRoot)) {
|
|
2347
|
+
if (!isRewritable(rel)) continue;
|
|
2348
|
+
const edits = retargets(repoRoot, rel, moved);
|
|
2349
|
+
if (!edits.length) continue;
|
|
2350
|
+
const path = join12(repoRoot, rel);
|
|
2351
|
+
let text = readFileSync10(path, "utf8");
|
|
2352
|
+
text = text.replace(LINK, (whole, href, anchor) => {
|
|
2353
|
+
const edit = edits.find((e) => e.href === href);
|
|
2354
|
+
return edit ? `](${edit.to}${anchor})` : whole;
|
|
2355
|
+
});
|
|
2356
|
+
writeFileSync5(path, text);
|
|
2357
|
+
}
|
|
2358
|
+
for (const m of plan.moves) {
|
|
2359
|
+
const to = join12(repoRoot, ...m.to.split("/"));
|
|
2360
|
+
mkdirSync5(dirname7(to), { recursive: true });
|
|
2361
|
+
renameSync(join12(repoRoot, m.from), to);
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
|
|
1780
2365
|
// src/cli.ts
|
|
1781
|
-
|
|
1782
|
-
var
|
|
2366
|
+
import { existsSync as existsSync10 } from "node:fs";
|
|
2367
|
+
var HERE = dirname8(fileURLToPath3(import.meta.url));
|
|
2368
|
+
var MODULES2 = join13(HERE, "..", "modules");
|
|
1783
2369
|
var c = {
|
|
1784
2370
|
dim: (s) => `\x1B[2m${s}\x1B[0m`,
|
|
1785
2371
|
bold: (s) => `\x1B[1m${s}\x1B[0m`,
|
|
@@ -1828,7 +2414,7 @@ ${mods.length} modules
|
|
|
1828
2414
|
const issues = auditModules(mods);
|
|
1829
2415
|
console.log();
|
|
1830
2416
|
if (issues.length === 0) {
|
|
1831
|
-
console.log(c.green(" audit clean") + c.dim(" \u2014 every parameter accounted for
|
|
2417
|
+
console.log(c.green(" audit clean") + c.dim(" \u2014 every parameter accounted for; every gate has a table, a why, and a declared applicability"));
|
|
1832
2418
|
} else {
|
|
1833
2419
|
console.log(c.red(` ${issues.length} issue(s):`));
|
|
1834
2420
|
for (const i of issues) console.log(` ${c.yellow(i.module)} ${c.dim(i.kind)} \u2014 ${i.detail}`);
|
|
@@ -1836,8 +2422,8 @@ ${mods.length} modules
|
|
|
1836
2422
|
console.log();
|
|
1837
2423
|
return issues.length === 0 ? 0 : 1;
|
|
1838
2424
|
}
|
|
1839
|
-
function cmdDoctor(target) {
|
|
1840
|
-
const root =
|
|
2425
|
+
function cmdDoctor(target, doExplain = false) {
|
|
2426
|
+
const root = resolve4(target);
|
|
1841
2427
|
const mods = loadAllModules(MODULES2);
|
|
1842
2428
|
console.log(c.bold(`
|
|
1843
2429
|
rungs doctor \u2014 ${root}
|
|
@@ -1906,6 +2492,9 @@ rungs doctor \u2014 ${root}
|
|
|
1906
2492
|
console.log(c.dim(" This reports presence, never quality. It cannot tell whether an adopted"));
|
|
1907
2493
|
console.log(c.dim(" system is good, complete, or working \u2014 only that files are where a"));
|
|
1908
2494
|
console.log(c.dim(" module's files would be. Signatures under-detect on purpose.\n"));
|
|
2495
|
+
reportLedger(root);
|
|
2496
|
+
if (doExplain) reportExplain(mods, results, root, files);
|
|
2497
|
+
else advertiseAnalysis(results);
|
|
1909
2498
|
const theirs = byState("theirs");
|
|
1910
2499
|
console.log(c.bold(" Next\n"));
|
|
1911
2500
|
if (ours) {
|
|
@@ -1931,6 +2520,58 @@ rungs doctor \u2014 ${root}
|
|
|
1931
2520
|
function firstSentence(s) {
|
|
1932
2521
|
return s.trim().replace(/\s+/g, " ").split(/(?<=\.)\s/)[0];
|
|
1933
2522
|
}
|
|
2523
|
+
function advertiseAnalysis(results) {
|
|
2524
|
+
const inScope = results.filter((r) => IN_SCOPE.has(r.state)).length;
|
|
2525
|
+
if (!inScope) return;
|
|
2526
|
+
console.log(c.bold(" Analysis\n"));
|
|
2527
|
+
console.log(` ${inScope} of these are things this repo already has, and can be checked against it.`);
|
|
2528
|
+
console.log(` ${c.cyan("rungs doctor --explain")} ${c.dim("\u2014 evidenced findings, and the incident behind each check")}
|
|
2529
|
+
`);
|
|
2530
|
+
}
|
|
2531
|
+
function reportExplain(mods, results, root, files) {
|
|
2532
|
+
const { reported, skipped, scope } = explain(mods, results, root, files);
|
|
2533
|
+
console.log(c.bold(" What it also checked\n"));
|
|
2534
|
+
if (!scope.length) {
|
|
2535
|
+
console.log(c.dim(" Nothing \u2014 detectors run only over what this repo already has, and"));
|
|
2536
|
+
console.log(c.dim(" detection found no equivalent of any module. There is nothing here to"));
|
|
2537
|
+
console.log(c.dim(" check that would not be checking our conventions against your repo.\n"));
|
|
2538
|
+
return;
|
|
2539
|
+
}
|
|
2540
|
+
const total = reported.reduce((n, r) => n + r.findings.length, 0);
|
|
2541
|
+
console.log(
|
|
2542
|
+
c.dim(` ran the detectors for ${scope.length} module(s) this repo already has: `) + c.dim(scope.join(" ")) + "\n"
|
|
2543
|
+
);
|
|
2544
|
+
for (const r of reported) {
|
|
2545
|
+
const n = r.findings.length;
|
|
2546
|
+
console.log(` ${c.yellow(r.gate.padEnd(34))} ${c.bold(String(n))} ${n === 1 ? "finding" : "findings"}`);
|
|
2547
|
+
for (const f of r.findings.slice(0, 4)) {
|
|
2548
|
+
console.log(c.dim(` ${f.file ? `${f.file}: ` : ""}${f.message}`));
|
|
2549
|
+
}
|
|
2550
|
+
if (n > 4) console.log(c.dim(` \u2026and ${n - 4} more`));
|
|
2551
|
+
if (r.why) console.log(c.dim(` why: ${firstSentence(r.why)}`));
|
|
2552
|
+
console.log();
|
|
2553
|
+
}
|
|
2554
|
+
if (!total) {
|
|
2555
|
+
console.log(c.dim(" No detector fired. That is not a clean bill of health \u2014 see below.\n"));
|
|
2556
|
+
}
|
|
2557
|
+
console.log(c.dim(" This is not an audit, and it is deliberately incomplete:"));
|
|
2558
|
+
console.log(c.dim(" \xB7 Detectors ran only for modules this repo already has an equivalent of."));
|
|
2559
|
+
console.log(c.dim(" \xB7 They read rungs-shaped inputs. A finding may be true and framed against"));
|
|
2560
|
+
console.log(c.dim(" a convention you never adopted \u2014 that is our defect, not yours."));
|
|
2561
|
+
if (skipped.command) {
|
|
2562
|
+
console.log(c.dim(` \xB7 ${skipped.command} command gate(s) not run. rungs does not execute commands in a repo it is only reading.`));
|
|
2563
|
+
}
|
|
2564
|
+
if (skipped.undeclared.length) {
|
|
2565
|
+
console.log(c.dim(` \xB7 ${skipped.undeclared.length} gate(s) never said whether they can read a repo like yours, so they did not: ${skipped.undeclared.join(" ")}`));
|
|
2566
|
+
}
|
|
2567
|
+
if (skipped.unimplemented.length) {
|
|
2568
|
+
console.log(c.dim(` \xB7 ${skipped.unimplemented.length} declared gate(s) have no engine and were skipped, never passed: ${skipped.unimplemented.join(" ")}`));
|
|
2569
|
+
}
|
|
2570
|
+
for (const e of skipped.errored) {
|
|
2571
|
+
console.log(c.dim(` \xB7 ${e.gate} could not run here (${e.message}) \u2014 a fact about this pass, not about your repo.`));
|
|
2572
|
+
}
|
|
2573
|
+
console.log();
|
|
2574
|
+
}
|
|
1934
2575
|
function cmdAdd(names, root, dryRun, harnesses, stamp) {
|
|
1935
2576
|
const mods = loadAllModules(MODULES2);
|
|
1936
2577
|
const { order, missing } = resolveInstallOrder(names, mods);
|
|
@@ -1963,9 +2604,50 @@ rungs add ${names.join(" ")} \u2192 ${root}${dryRun ? c.yellow(" (dry run)") :
|
|
|
1963
2604
|
`));
|
|
1964
2605
|
if (pulled.length) console.log(c.dim(` pulled in by dependency: ${pulled.map((m) => m.name).join(", ")}
|
|
1965
2606
|
`));
|
|
2607
|
+
const scanned = scanRepo(root);
|
|
2608
|
+
const paradigms = new Set(
|
|
2609
|
+
order.map((m) => detect(m, root, scanned)).filter((r) => r.state === "paradigm").map((r) => r.module)
|
|
2610
|
+
);
|
|
2611
|
+
const overridden = flags.has("--confirm-paradigm");
|
|
2612
|
+
const blocked = overridden ? /* @__PURE__ */ new Map() : blockedByParadigm(order, paradigms);
|
|
2613
|
+
if (overridden && paradigms.size) {
|
|
2614
|
+
for (const name of paradigms) {
|
|
2615
|
+
const p = detect(order.find((m) => m.name === name), root, scanned).paradigm;
|
|
2616
|
+
console.log(
|
|
2617
|
+
c.yellow(` ${name}: installing over an existing ${p.id}`) + c.dim(` (${p.matched[0]}) \u2014 --confirm-paradigm`)
|
|
2618
|
+
);
|
|
2619
|
+
}
|
|
2620
|
+
console.log(c.dim(" You will have two systems for one job. That is a choice, not a merge.\n"));
|
|
2621
|
+
}
|
|
2622
|
+
let toInstall = order;
|
|
2623
|
+
if (blocked.size) {
|
|
2624
|
+
for (const mod of order) {
|
|
2625
|
+
const cause = blocked.get(mod.name);
|
|
2626
|
+
if (!cause) continue;
|
|
2627
|
+
if (cause === mod.name) {
|
|
2628
|
+
const p = detect(mod, root, scanned).paradigm;
|
|
2629
|
+
console.log(c.yellow(` ${mod.name}: this repo already does this another way \u2014 ${p.id}`));
|
|
2630
|
+
console.log(c.dim(` matched ${p.matched[0]}`));
|
|
2631
|
+
for (const line of (p.note ?? "").trim().split("\n")) console.log(c.dim(` ${line || ""}`));
|
|
2632
|
+
if (p.compare) console.log(c.dim(` compare: ${p.compare}`));
|
|
2633
|
+
} else {
|
|
2634
|
+
console.log(c.yellow(` ${mod.name}: not installed \u2014 it requires ${cause}.`));
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
toInstall = resolveInstallOrder(names.filter((n) => !blocked.has(n)), mods).order;
|
|
2638
|
+
const dropped = order.filter((m) => !toInstall.includes(m) && !blocked.has(m.name));
|
|
2639
|
+
if (dropped.length) {
|
|
2640
|
+
console.log(c.dim(` ${dropped.map((m) => m.name).join(", ")} not written \u2014 pulled in only for the above`));
|
|
2641
|
+
}
|
|
2642
|
+
console.log(
|
|
2643
|
+
c.dim(`
|
|
2644
|
+
Pass --confirm-paradigm to install anyway.`) + (toInstall.length ? c.dim(" Continuing with the rest.\n") : c.dim(" Nothing was written.\n"))
|
|
2645
|
+
);
|
|
2646
|
+
if (!toInstall.length) return 1;
|
|
2647
|
+
}
|
|
1966
2648
|
const installed = [];
|
|
1967
2649
|
const wrote = /* @__PURE__ */ new Map();
|
|
1968
|
-
for (const mod of
|
|
2650
|
+
for (const mod of toInstall) {
|
|
1969
2651
|
if (mod.threshold?.confirm && !dryRun && !flags.has("--confirm-threshold")) {
|
|
1970
2652
|
console.log(
|
|
1971
2653
|
c.yellow(` ${mod.name}: requires ${mod.threshold.minimum}+ ${mod.threshold.metric}.`) + c.dim(" Skipped \u2014 pass --confirm-threshold to install it.\n")
|
|
@@ -2031,9 +2713,26 @@ rungs render \u2014 ${root}
|
|
|
2031
2713
|
return 0;
|
|
2032
2714
|
}
|
|
2033
2715
|
function cmdCheck(root, tier, stamp) {
|
|
2034
|
-
|
|
2716
|
+
let runs;
|
|
2717
|
+
try {
|
|
2718
|
+
runs = runGates(root, tier);
|
|
2719
|
+
} catch (e) {
|
|
2720
|
+
if (!(e instanceof UnknownTierError)) throw e;
|
|
2721
|
+
console.log(c.yellow(`
|
|
2722
|
+
unknown tier "${e.requested}"`) + c.dim(` \u2014 this repo declares ${e.declared.join(", ")}.`));
|
|
2723
|
+
console.log(c.dim(" Nothing ran. Use `rungs check` to run every registered gate.\n"));
|
|
2724
|
+
return 1;
|
|
2725
|
+
}
|
|
2035
2726
|
if (!runs.length) {
|
|
2036
|
-
|
|
2727
|
+
const runnable = loadRegistry(root).gates.filter((g) => !g.trigger);
|
|
2728
|
+
if (runnable.length && tier) {
|
|
2729
|
+
const tiers = [...new Set(runnable.map((g) => g.tier).filter(Boolean))];
|
|
2730
|
+
console.log(c.yellow(`
|
|
2731
|
+
no gates in the ${tier} tier \u2014 ${runnable.length} are registered`) + c.dim(` (${tiers.length ? tiers.join(", ") : "none tiered"}).`));
|
|
2732
|
+
console.log(c.dim(" Nothing ran. Use `rungs check` to run every registered gate.\n"));
|
|
2733
|
+
} else {
|
|
2734
|
+
console.log(c.yellow("\n no gates registered \u2014 is this a rungs repo?\n"));
|
|
2735
|
+
}
|
|
2037
2736
|
return 1;
|
|
2038
2737
|
}
|
|
2039
2738
|
appendLedger(root, runs, stamp);
|
|
@@ -2060,26 +2759,67 @@ rungs check \u2014 ${root}${tier ? ` (${tier} tier)` : ""}
|
|
|
2060
2759
|
c.yellow("\n Unimplemented gates are not passes.") + c.dim(" A registry reporting green because most of its\n gates do nothing is the worst failure this tool could have, so they block.")
|
|
2061
2760
|
);
|
|
2062
2761
|
}
|
|
2762
|
+
console.log();
|
|
2763
|
+
return n("fail") + n("unimplemented") + n("error") > 0 ? 1 : 0;
|
|
2764
|
+
}
|
|
2765
|
+
function reportLedger(root) {
|
|
2063
2766
|
const { gates } = loadRegistry(root);
|
|
2064
2767
|
const q = ledgerQuestions(root, gates);
|
|
2065
|
-
if (q.neverFired.length
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
console.log(c.dim(" Is that still a risk here, or is the gate scoped too narrowly?"));
|
|
2071
|
-
}
|
|
2072
|
-
for (const g of q.alwaysFires.slice(0, 3)) {
|
|
2073
|
-
console.log(` ${c.cyan(g.id)} fails ${g.rate}. ${c.dim("Red by default is a gate people learn to bypass.")}`);
|
|
2074
|
-
}
|
|
2075
|
-
console.log(
|
|
2076
|
-
c.dim("\n These are questions, not verdicts. The ledger records whether a gate ran")
|
|
2077
|
-
);
|
|
2078
|
-
console.log(c.dim(" and whether it fired \u2014 never whether it is valuable. Gates invoked"));
|
|
2079
|
-
console.log(c.dim(" directly, and CI runs, are not counted."));
|
|
2768
|
+
if (!q.neverFired.length && !q.alwaysFires.length) return;
|
|
2769
|
+
console.log(c.bold(` Ledger questions ${c.dim(`(${q.runs} recorded runs)`)}`));
|
|
2770
|
+
for (const g of q.neverFired.slice(0, 3)) {
|
|
2771
|
+
console.log(` ${c.cyan(g.id)} has never fired. ${c.dim(firstSentence(g.why ?? ""))}`);
|
|
2772
|
+
console.log(c.dim(" Is that still a risk here, or is the gate scoped too narrowly?"));
|
|
2080
2773
|
}
|
|
2081
|
-
|
|
2082
|
-
|
|
2774
|
+
for (const g of q.alwaysFires.slice(0, 3)) {
|
|
2775
|
+
console.log(` ${c.cyan(g.id)} fails ${g.rate}. ${c.dim("Red by default is a gate people learn to bypass.")}`);
|
|
2776
|
+
}
|
|
2777
|
+
console.log(c.dim("\n These are questions, not verdicts. The ledger records whether a gate ran"));
|
|
2778
|
+
console.log(c.dim(" and whether it fired \u2014 never whether it is valuable. Gates invoked"));
|
|
2779
|
+
console.log(c.dim(" directly, and CI runs, are not counted.\n"));
|
|
2780
|
+
}
|
|
2781
|
+
function cmdBacklogArchive(root, dryRun) {
|
|
2782
|
+
const record = readRecord(root);
|
|
2783
|
+
const configured = record?.modules["backlog"]?.params?.root;
|
|
2784
|
+
const backlogRoot = `docs/${configured ?? "backlog"}`;
|
|
2785
|
+
if (!existsSync10(join13(root, ...backlogRoot.split("/"), "items"))) {
|
|
2786
|
+
console.log(c.red(`
|
|
2787
|
+
no backlog at ${backlogRoot}/items
|
|
2788
|
+
`));
|
|
2789
|
+
return 1;
|
|
2790
|
+
}
|
|
2791
|
+
const plan = planArchive(root, backlogRoot);
|
|
2792
|
+
console.log(c.bold(`
|
|
2793
|
+
rungs backlog archive \u2192 ${root}${dryRun ? c.yellow(" (dry run)") : ""}
|
|
2794
|
+
`));
|
|
2795
|
+
for (const h of plan.held) console.log(c.yellow(` held ${h.file}`) + c.dim(` \u2014 ${h.reason}`));
|
|
2796
|
+
if (plan.held.length) console.log();
|
|
2797
|
+
if (!plan.moves.length) {
|
|
2798
|
+
console.log(c.dim(" nothing to archive \u2014 no item is done or rejected.\n"));
|
|
2799
|
+
return 0;
|
|
2800
|
+
}
|
|
2801
|
+
const byStatus = /* @__PURE__ */ new Map();
|
|
2802
|
+
for (const m of plan.moves) byStatus.set(m.status, (byStatus.get(m.status) ?? 0) + 1);
|
|
2803
|
+
console.log(
|
|
2804
|
+
` ${c.bold(String(plan.moves.length))} item(s) \u2014 ${[...byStatus].map(([s, n]) => `${n} ${s}`).join(" \xB7 ")}`
|
|
2805
|
+
);
|
|
2806
|
+
for (const m of plan.moves.slice(0, 5)) console.log(c.dim(` ${m.from} \u2192 ${m.to}`));
|
|
2807
|
+
if (plan.moves.length > 5) console.log(c.dim(` \u2026and ${plan.moves.length - 5} more`));
|
|
2808
|
+
const touched = plan.rewrites.filter((r) => r.links);
|
|
2809
|
+
const links = touched.reduce((n, r) => n + r.links, 0);
|
|
2810
|
+
console.log(`
|
|
2811
|
+
${c.bold(String(links))} link(s) repointed across ${touched.length} file(s)`);
|
|
2812
|
+
for (const r of touched.slice(0, 5)) console.log(c.dim(` ${r.file} (${r.links})`));
|
|
2813
|
+
if (touched.length > 5) console.log(c.dim(` \u2026and ${touched.length - 5} more`));
|
|
2814
|
+
if (dryRun) {
|
|
2815
|
+
console.log(c.dim("\n Nothing written. Drop --dry-run to apply.\n"));
|
|
2816
|
+
return 0;
|
|
2817
|
+
}
|
|
2818
|
+
applyArchive(root, plan);
|
|
2819
|
+
console.log(c.green(`
|
|
2820
|
+
archived ${plan.moves.length} item(s)`) + c.dim(" \u2014 ids stay spent and every citation still resolves."));
|
|
2821
|
+
console.log(c.dim(" Run `rungs check` to confirm.\n"));
|
|
2822
|
+
return 0;
|
|
2083
2823
|
}
|
|
2084
2824
|
function cmdInit(root, profile, dryRun, harnesses, stamp) {
|
|
2085
2825
|
if (readRecord(root)) {
|
|
@@ -2122,10 +2862,15 @@ rungs upgrade \u2014 ${root}${apply ? "" : c.yellow(" (preview)")}
|
|
|
2122
2862
|
console.log(` ${c.yellow("diverged")} ${f.rel} ${c.dim("\u2014 yours, left alone")}`);
|
|
2123
2863
|
}
|
|
2124
2864
|
}
|
|
2125
|
-
if (apply
|
|
2126
|
-
const written = applyUpgrade(root, mods, record, plan);
|
|
2865
|
+
if (apply) {
|
|
2866
|
+
const { written, gates, recorded } = applyUpgrade(root, mods, record, plan);
|
|
2867
|
+
const parts = [
|
|
2868
|
+
written ? `${written} file(s)` : "",
|
|
2869
|
+
gates ? `${gates} gate registration(s)` : "",
|
|
2870
|
+
recorded ? `${recorded} record line(s)` : ""
|
|
2871
|
+
].filter(Boolean);
|
|
2127
2872
|
console.log(c.green(`
|
|
2128
|
-
updated ${
|
|
2873
|
+
updated ${parts.length ? parts.join(" \xB7 ") : "nothing"}`));
|
|
2129
2874
|
}
|
|
2130
2875
|
console.log(
|
|
2131
2876
|
`
|
|
@@ -2161,10 +2906,13 @@ var COMMANDS = [
|
|
|
2161
2906
|
["upgrade [path]", "move to newer module versions, never touching what you edited"],
|
|
2162
2907
|
["eject [path]", "materialise the engines; stop depending on rungs"],
|
|
2163
2908
|
["setup git [path]", "install the merge drivers .gitattributes names"],
|
|
2164
|
-
["modules", "list the module set and audit the manifests"]
|
|
2909
|
+
["modules", "list the module set and audit the manifests"],
|
|
2910
|
+
["backlog archive [path]", "move finished items to archive/, repointing every link"]
|
|
2165
2911
|
];
|
|
2166
2912
|
var FLAGS = [
|
|
2167
2913
|
["--dry-run", "report what would happen, write nothing"],
|
|
2914
|
+
["--explain", "doctor: also run the detectors over what this repo already has"],
|
|
2915
|
+
["--confirm-paradigm", "add: install a module this repo already solves another way"],
|
|
2168
2916
|
["--into <path>", "add: install into this repo instead of the working directory"],
|
|
2169
2917
|
["--set m.param=value", "add/init: override a module parameter. Repeatable"],
|
|
2170
2918
|
["--confirm-threshold", "add: install a module whose rung is above this repo"],
|
|
@@ -2231,21 +2979,29 @@ switch (cmd) {
|
|
|
2231
2979
|
case "modules":
|
|
2232
2980
|
process.exit(cmdModules(flags.has("--params")));
|
|
2233
2981
|
case "doctor":
|
|
2234
|
-
process.exit(cmdDoctor(args[0] ?? process.cwd()));
|
|
2982
|
+
process.exit(cmdDoctor(args[0] ?? process.cwd(), flags.has("--explain")));
|
|
2983
|
+
case "backlog": {
|
|
2984
|
+
if (args[0] !== "archive") {
|
|
2985
|
+
console.log(c.red(`
|
|
2986
|
+
unknown: rungs backlog ${args[0] ?? ""}`) + c.dim("\n The only subcommand is `archive`.\n"));
|
|
2987
|
+
process.exit(1);
|
|
2988
|
+
}
|
|
2989
|
+
process.exit(cmdBacklogArchive(resolve4(args[1] ?? process.cwd()), flags.has("--dry-run")));
|
|
2990
|
+
}
|
|
2235
2991
|
case "check": {
|
|
2236
2992
|
const tier = args[1] ?? (flags.has("--full") ? "full" : flags.has("--fast") ? "fast" : void 0);
|
|
2237
|
-
process.exit(cmdCheck(
|
|
2993
|
+
process.exit(cmdCheck(resolve4(args[0] ?? process.cwd()), tier, STAMP));
|
|
2238
2994
|
}
|
|
2239
2995
|
case "init": {
|
|
2240
2996
|
const profile = args[1] ?? "tracked";
|
|
2241
|
-
process.exit(cmdInit(
|
|
2997
|
+
process.exit(cmdInit(resolve4(args[0] ?? process.cwd()), profile, flags.has("--dry-run"), HARNESSES, STAMP));
|
|
2242
2998
|
}
|
|
2243
2999
|
case "upgrade":
|
|
2244
|
-
process.exit(cmdUpgrade(
|
|
3000
|
+
process.exit(cmdUpgrade(resolve4(args[0] ?? process.cwd()), flags.has("--apply")));
|
|
2245
3001
|
case "eject":
|
|
2246
|
-
process.exit(cmdEject(
|
|
3002
|
+
process.exit(cmdEject(resolve4(args[0] ?? process.cwd()), flags.has("--dry-run")));
|
|
2247
3003
|
case "setup": {
|
|
2248
|
-
const r = setupGit(
|
|
3004
|
+
const r = setupGit(resolve4(args[1] ?? process.cwd()), flags.has("--dry-run"));
|
|
2249
3005
|
console.log(
|
|
2250
3006
|
r.drivers.length ? `
|
|
2251
3007
|
installed ${r.drivers.length} merge driver(s): ${r.drivers.join(", ")}` + (r.rerere ? c.dim(" \xB7 rerere on") : "") + c.dim("\n Declared drivers were inert until now \u2014 a fresh clone needs this once.\n") : c.dim("\n no rungs merge drivers declared in .gitattributes\n")
|
|
@@ -2253,11 +3009,11 @@ switch (cmd) {
|
|
|
2253
3009
|
process.exit(0);
|
|
2254
3010
|
}
|
|
2255
3011
|
case "render":
|
|
2256
|
-
process.exit(cmdRender(
|
|
3012
|
+
process.exit(cmdRender(resolve4(args[0] ?? process.cwd()), HARNESSES, STAMP));
|
|
2257
3013
|
case "add": {
|
|
2258
3014
|
const target = flags.has("--into") ? args[args.length - 1] : process.cwd();
|
|
2259
3015
|
const names = flags.has("--into") ? args.slice(0, -1) : args;
|
|
2260
|
-
process.exit(cmdAdd(names,
|
|
3016
|
+
process.exit(cmdAdd(names, resolve4(target), flags.has("--dry-run"), HARNESSES, STAMP));
|
|
2261
3017
|
}
|
|
2262
3018
|
default: {
|
|
2263
3019
|
const wantedHelp = cmd === void 0 || cmd === "help" || cmd === "--help" || cmd === "-h";
|