@geonosis/ledger 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +387 -0
- package/bin/geonosis-ledger.mjs +4 -0
- package/dist/chunk-WI3N5B3J.js +1538 -0
- package/dist/index.d.ts +469 -0
- package/dist/index.js +110 -0
- package/dist/ledger-cli.js +339 -0
- package/package.json +43 -0
|
@@ -0,0 +1,1538 @@
|
|
|
1
|
+
// src/args.ts
|
|
2
|
+
var parseArgs = (argv, valued) => {
|
|
3
|
+
const flags = {};
|
|
4
|
+
const positional = [];
|
|
5
|
+
const switches = /* @__PURE__ */ new Set();
|
|
6
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
7
|
+
const token = argv[index] ?? "";
|
|
8
|
+
if (!token.startsWith("--")) {
|
|
9
|
+
positional.push(token);
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
const inline = token.indexOf("=");
|
|
13
|
+
if (inline !== -1) {
|
|
14
|
+
const name = token.slice(0, inline);
|
|
15
|
+
if (!valued.has(name)) throw new Error(`${name} takes no value`);
|
|
16
|
+
flags[name] = token.slice(inline + 1);
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (!valued.has(token)) {
|
|
20
|
+
switches.add(token);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const value = argv[index + 1];
|
|
24
|
+
if (value === void 0) throw new Error(`${token} needs a value`);
|
|
25
|
+
flags[token] = value;
|
|
26
|
+
index += 1;
|
|
27
|
+
}
|
|
28
|
+
return { flags, positional, switches };
|
|
29
|
+
};
|
|
30
|
+
var required = (args, flag) => {
|
|
31
|
+
const value = args.flags[flag];
|
|
32
|
+
if (value === void 0 || value === "") throw new Error(`${flag} is required`);
|
|
33
|
+
return value;
|
|
34
|
+
};
|
|
35
|
+
var rejectUnknownSwitches = (args, known) => {
|
|
36
|
+
for (const one of args.switches) {
|
|
37
|
+
if (!known.has(one)) throw new Error(`unknown argument "${one}"`);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// src/config.ts
|
|
42
|
+
import { existsSync, readFileSync } from "fs";
|
|
43
|
+
import { resolve } from "path";
|
|
44
|
+
var RUNTIME_DEFAULT = {
|
|
45
|
+
exclude: [
|
|
46
|
+
"\\.(?:test|spec)\\.",
|
|
47
|
+
"\\.stories\\.",
|
|
48
|
+
"\\.d\\.ts$",
|
|
49
|
+
"(?:^|/)(?:__tests__|__mocks__|__fixtures__|__snapshots__|__perf__)/",
|
|
50
|
+
"(?:^|/)(?:docs?|tests?|e2e)/",
|
|
51
|
+
"(?:^|/)\\.[^/]+/"
|
|
52
|
+
],
|
|
53
|
+
include: ["\\.(?:ts|tsx|js|jsx|mjs|cjs|mts|cts|css|scss|sass|less|svg|vue|svelte|astro)$"]
|
|
54
|
+
};
|
|
55
|
+
var PLAN_DEFAULT = {
|
|
56
|
+
criteria: "^- When .+ shall .+",
|
|
57
|
+
// `\b`, not `$`: real plans qualify the heading — "## Acceptance criteria (EARS)" is what all 21
|
|
58
|
+
// of this repo's carry, and anchoring to end-of-line reported every one of them as missing it.
|
|
59
|
+
criteriaHeading: "^#{2,3} +Acceptance criteria\\b",
|
|
60
|
+
// The capture group, when there is one, is the STATUS WORD — what `status` and `handoff` group
|
|
61
|
+
// plans by. A pattern with no group still validates; those plans simply have no named status.
|
|
62
|
+
statusHeader: "^> \\*\\*Status\\*\\*: *(\\S+)",
|
|
63
|
+
verificationHeading: "^#{2,3} +Verification\\b"
|
|
64
|
+
};
|
|
65
|
+
var LEDGER_DEFAULT = {
|
|
66
|
+
agents: "AGENTS.md",
|
|
67
|
+
architecture: "docs/architecture.md",
|
|
68
|
+
// Subject plus three body lines. A delivery whose message needs a fourth is a delivery that
|
|
69
|
+
// should have been two, or a paragraph that belongs in a proof file.
|
|
70
|
+
commitMessageLines: 4,
|
|
71
|
+
decisions: "docs/decisions.md",
|
|
72
|
+
handoff: "HANDOFF.md",
|
|
73
|
+
journal: "docs/journal/fallbacks.md",
|
|
74
|
+
law: "CLAUDE.md",
|
|
75
|
+
// The heading, not the whole file: AGENTS.md carries the rules, and a copy of the law is a
|
|
76
|
+
// second law. `\\b` rather than `$` for the same reason `criteriaHeading` uses it — real
|
|
77
|
+
// headings are qualified.
|
|
78
|
+
lawSection: "^#{2,3} +The laws\\b",
|
|
79
|
+
maxLines: 200,
|
|
80
|
+
plan: PLAN_DEFAULT,
|
|
81
|
+
plans: "plans",
|
|
82
|
+
progress: "plans/PROGRESS.md",
|
|
83
|
+
proofs: "proofs",
|
|
84
|
+
rules: ".claude/rules",
|
|
85
|
+
runtime: RUNTIME_DEFAULT,
|
|
86
|
+
skills: ".claude/skills"
|
|
87
|
+
};
|
|
88
|
+
var PATH_KEYS = [
|
|
89
|
+
"agents",
|
|
90
|
+
"architecture",
|
|
91
|
+
"decisions",
|
|
92
|
+
"handoff",
|
|
93
|
+
"journal",
|
|
94
|
+
"law",
|
|
95
|
+
"plans",
|
|
96
|
+
"progress",
|
|
97
|
+
"proofs",
|
|
98
|
+
"rules",
|
|
99
|
+
"skills"
|
|
100
|
+
];
|
|
101
|
+
var NUMBER_KEYS = ["commitMessageLines", "maxLines"];
|
|
102
|
+
var KNOWN = /* @__PURE__ */ new Set([...PATH_KEYS, ...NUMBER_KEYS, "lawSection", "plan", "runtime"]);
|
|
103
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
104
|
+
var compileAll = (patterns, where) => {
|
|
105
|
+
for (const pattern of patterns) {
|
|
106
|
+
try {
|
|
107
|
+
RegExp(pattern);
|
|
108
|
+
} catch {
|
|
109
|
+
throw new Error(`${where}: "${pattern}" is not a regular expression`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
var readPatterns = (value, fallback, where) => {
|
|
114
|
+
if (value === void 0) return fallback;
|
|
115
|
+
if (!isRecord(value)) throw new Error(`${where} must be an object with include/exclude`);
|
|
116
|
+
const read = (key) => {
|
|
117
|
+
const raw = value[key];
|
|
118
|
+
if (raw === void 0) return fallback[key];
|
|
119
|
+
if (!Array.isArray(raw) || raw.some((one) => typeof one !== "string")) {
|
|
120
|
+
throw new Error(`${where}.${key} must be a list of regular expressions`);
|
|
121
|
+
}
|
|
122
|
+
const list = raw;
|
|
123
|
+
compileAll(list, `${where}.${key}`);
|
|
124
|
+
return list;
|
|
125
|
+
};
|
|
126
|
+
return { exclude: read("exclude"), include: read("include") };
|
|
127
|
+
};
|
|
128
|
+
var readPlan = (value) => {
|
|
129
|
+
if (value === void 0) return PLAN_DEFAULT;
|
|
130
|
+
if (!isRecord(value)) throw new Error("ledger.plan must be an object");
|
|
131
|
+
const out = { ...PLAN_DEFAULT };
|
|
132
|
+
for (const key of Object.keys(value)) {
|
|
133
|
+
if (!(key in PLAN_DEFAULT)) {
|
|
134
|
+
throw new Error(`ledger.plan.${key} is not a key of the plan contract`);
|
|
135
|
+
}
|
|
136
|
+
const pattern = value[key];
|
|
137
|
+
if (typeof pattern !== "string") throw new Error(`ledger.plan.${key} must be a string`);
|
|
138
|
+
compileAll([pattern], `ledger.plan.${key}`);
|
|
139
|
+
out[key] = pattern;
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
};
|
|
143
|
+
var loadLedgerConfig = (root) => {
|
|
144
|
+
const file = resolve(root, "geonosis.json");
|
|
145
|
+
if (!existsSync(file)) return LEDGER_DEFAULT;
|
|
146
|
+
let parsed;
|
|
147
|
+
try {
|
|
148
|
+
parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
149
|
+
} catch (error) {
|
|
150
|
+
throw new Error(`geonosis.json is not valid JSON: ${error.message}`, {
|
|
151
|
+
cause: error
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
if (!isRecord(parsed)) throw new Error("geonosis.json must hold an object");
|
|
155
|
+
const block = parsed["ledger"];
|
|
156
|
+
if (block === void 0) return LEDGER_DEFAULT;
|
|
157
|
+
if (!isRecord(block)) throw new Error("ledger must be an object");
|
|
158
|
+
for (const key of Object.keys(block)) {
|
|
159
|
+
if (!KNOWN.has(key)) throw new Error(`ledger.${key} is not a key of the ledger block`);
|
|
160
|
+
}
|
|
161
|
+
const config = { ...LEDGER_DEFAULT };
|
|
162
|
+
for (const key of PATH_KEYS) {
|
|
163
|
+
const value = block[key];
|
|
164
|
+
if (value === void 0) continue;
|
|
165
|
+
if (typeof value !== "string") throw new Error(`ledger.${key} must be a path`);
|
|
166
|
+
config[key] = value;
|
|
167
|
+
}
|
|
168
|
+
for (const key of NUMBER_KEYS) {
|
|
169
|
+
const value = block[key];
|
|
170
|
+
if (value === void 0) continue;
|
|
171
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
|
|
172
|
+
throw new Error(`ledger.${key} must be a whole number of at least 1`);
|
|
173
|
+
}
|
|
174
|
+
config[key] = value;
|
|
175
|
+
}
|
|
176
|
+
const section = block["lawSection"];
|
|
177
|
+
if (section !== void 0) {
|
|
178
|
+
if (typeof section !== "string") throw new Error("ledger.lawSection must be a string");
|
|
179
|
+
compileAll([section], "ledger.lawSection");
|
|
180
|
+
config.lawSection = section;
|
|
181
|
+
}
|
|
182
|
+
config.plan = readPlan(block["plan"]);
|
|
183
|
+
config.runtime = readPatterns(block["runtime"], RUNTIME_DEFAULT, "ledger.runtime");
|
|
184
|
+
return config;
|
|
185
|
+
};
|
|
186
|
+
var ledgerOwned = (config) => [
|
|
187
|
+
config.architecture,
|
|
188
|
+
config.decisions,
|
|
189
|
+
config.handoff,
|
|
190
|
+
config.journal,
|
|
191
|
+
config.plans,
|
|
192
|
+
config.progress,
|
|
193
|
+
config.proofs
|
|
194
|
+
];
|
|
195
|
+
var matchesRuntime = (file, runtime) => {
|
|
196
|
+
const path = file.replaceAll("\\", "/");
|
|
197
|
+
if (!runtime.include.some((pattern) => new RegExp(pattern).test(path))) return false;
|
|
198
|
+
return !runtime.exclude.some((pattern) => new RegExp(pattern).test(path));
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// src/markdown.ts
|
|
202
|
+
var HEADING = /^(#{1,6}) +(.*)$/;
|
|
203
|
+
var headingsOf = (lines2) => {
|
|
204
|
+
const found = [];
|
|
205
|
+
let fenced = false;
|
|
206
|
+
for (const [index, line] of lines2.entries()) {
|
|
207
|
+
if (line.startsWith("```") || line.startsWith("~~~")) fenced = !fenced;
|
|
208
|
+
if (fenced) continue;
|
|
209
|
+
const match = HEADING.exec(line);
|
|
210
|
+
if (match?.[1] !== void 0 && match[2] !== void 0) {
|
|
211
|
+
found.push({ depth: match[1].length, index, text: match[2].trim() });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return found;
|
|
215
|
+
};
|
|
216
|
+
var sectionUnder = (lines2, pattern) => {
|
|
217
|
+
const test = new RegExp(pattern);
|
|
218
|
+
const headings = headingsOf(lines2);
|
|
219
|
+
const at = headings.find((heading) => test.test(lines2[heading.index] ?? ""));
|
|
220
|
+
if (at === void 0) return void 0;
|
|
221
|
+
const next = headings.find((heading) => heading.index > at.index && heading.depth <= at.depth);
|
|
222
|
+
return lines2.slice(at.index + 1, next?.index ?? lines2.length);
|
|
223
|
+
};
|
|
224
|
+
var cellsOf = (line) => line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((cell2) => cell2.trim());
|
|
225
|
+
var isDivider = (line) => /^\|?[\s:|-]+\|[\s:|-]*$/.test(line.trim()) && line.includes("-");
|
|
226
|
+
var firstTable = (lines2) => {
|
|
227
|
+
for (const [index, line] of lines2.entries()) {
|
|
228
|
+
if (!line.trim().startsWith("|")) continue;
|
|
229
|
+
const divider = lines2[index + 1];
|
|
230
|
+
if (divider === void 0 || !isDivider(divider)) continue;
|
|
231
|
+
const rows = [];
|
|
232
|
+
for (let at = index + 2; at < lines2.length; at += 1) {
|
|
233
|
+
const row = lines2[at];
|
|
234
|
+
if (row === void 0) break;
|
|
235
|
+
if (row.trim() === "") {
|
|
236
|
+
if (lines2[at + 1]?.trim().startsWith("|") !== true) break;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (!row.trim().startsWith("|")) break;
|
|
240
|
+
rows.push(at);
|
|
241
|
+
}
|
|
242
|
+
return { align: divider, header: cellsOf(line), headerIndex: index, rows };
|
|
243
|
+
}
|
|
244
|
+
return void 0;
|
|
245
|
+
};
|
|
246
|
+
var rowCells = (line) => cellsOf(line);
|
|
247
|
+
var plainCell = (cell2) => cell2.replace(/^\*\*(.*)\*\*$/, "$1").replaceAll("`", "").trim();
|
|
248
|
+
var renderRow = (cells) => `| ${cells.join(" | ")} |`;
|
|
249
|
+
|
|
250
|
+
// src/plan.ts
|
|
251
|
+
import { existsSync as existsSync2, mkdirSync, readdirSync, readFileSync as readFileSync2, statSync, writeFileSync } from "fs";
|
|
252
|
+
import { basename, join, relative, resolve as resolve2 } from "path";
|
|
253
|
+
var PLAN_FILE = /^(\d{3,})-([a-z\d][a-z\d.-]*)\.md$/;
|
|
254
|
+
var planFilesIn = (dir) => readdirSync(dir).filter((name) => PLAN_FILE.test(name)).toSorted().map((name) => join(dir, name));
|
|
255
|
+
var numberOf = (file) => Number(PLAN_FILE.exec(basename(file))?.[1] ?? Number.NaN);
|
|
256
|
+
var headingName = (pattern) => pattern.replace(/^\^?#\{[\d,]+\} ?\+? ?/, "").replace(/\\b.*$/, "").replace(/ ?\*?\$$/, "").replaceAll("\\", "").trim();
|
|
257
|
+
var violationsOf = (file, body, config) => {
|
|
258
|
+
const lines2 = body.split("\n");
|
|
259
|
+
const found = [];
|
|
260
|
+
const { criteria, criteriaHeading, statusHeader, verificationHeading } = config.plan;
|
|
261
|
+
const status = new RegExp(statusHeader);
|
|
262
|
+
if (!lines2.some((line) => status.test(line))) found.push(`${file}: no status header`);
|
|
263
|
+
const criteriaSection = sectionUnder(lines2, criteriaHeading);
|
|
264
|
+
if (criteriaSection === void 0) {
|
|
265
|
+
found.push(`${file}: no "${headingName(criteriaHeading)}" heading`);
|
|
266
|
+
} else if (!criteriaSection.some((line) => new RegExp(criteria).test(line))) {
|
|
267
|
+
found.push(`${file}: no EARS criterion under "${headingName(criteriaHeading)}"`);
|
|
268
|
+
}
|
|
269
|
+
if (sectionUnder(lines2, verificationHeading) === void 0) {
|
|
270
|
+
found.push(`${file}: no "${headingName(verificationHeading)}" heading`);
|
|
271
|
+
}
|
|
272
|
+
return found;
|
|
273
|
+
};
|
|
274
|
+
var checkPlans = ({ config, root, since, target }) => {
|
|
275
|
+
const where = resolve2(root, target ?? config.plans);
|
|
276
|
+
if (!existsSync2(where)) throw new Error(`${target ?? config.plans}: no such file or directory`);
|
|
277
|
+
const files = statSync(where).isDirectory() ? planFilesIn(where) : [where];
|
|
278
|
+
const legacy = [];
|
|
279
|
+
const violations = [];
|
|
280
|
+
let checked = 0;
|
|
281
|
+
for (const file of files) {
|
|
282
|
+
const named = relative(root, file).replaceAll("\\", "/");
|
|
283
|
+
if (since !== void 0 && numberOf(file) < since) {
|
|
284
|
+
legacy.push(named);
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
checked += 1;
|
|
288
|
+
violations.push(...violationsOf(named, readFileSync2(file, "utf8"), config));
|
|
289
|
+
}
|
|
290
|
+
return { checked, legacy, violations };
|
|
291
|
+
};
|
|
292
|
+
var planStatuses = ({
|
|
293
|
+
config,
|
|
294
|
+
root
|
|
295
|
+
}) => {
|
|
296
|
+
const dir = resolve2(root, config.plans);
|
|
297
|
+
if (!existsSync2(dir)) return [];
|
|
298
|
+
const test = new RegExp(config.plan.statusHeader);
|
|
299
|
+
return planFilesIn(dir).map((file) => {
|
|
300
|
+
const found = readFileSync2(file, "utf8").split("\n").map((line) => test.exec(line)).find((match) => match !== null);
|
|
301
|
+
return {
|
|
302
|
+
file: relative(root, file).replaceAll("\\", "/"),
|
|
303
|
+
status: found?.[1] ?? (found === void 0 ? "UNDECLARED" : "\u2014")
|
|
304
|
+
};
|
|
305
|
+
});
|
|
306
|
+
};
|
|
307
|
+
var nextNumber = (dir) => {
|
|
308
|
+
const highest = existsSync2(dir) ? planFilesIn(dir).reduce((most, file) => Math.max(most, numberOf(file)), 0) : 0;
|
|
309
|
+
return String(highest + 1).padStart(3, "0");
|
|
310
|
+
};
|
|
311
|
+
var KEBAB = /^[a-z\d][a-z\d-]*$/;
|
|
312
|
+
var newPlan = ({
|
|
313
|
+
config,
|
|
314
|
+
root,
|
|
315
|
+
slug
|
|
316
|
+
}) => {
|
|
317
|
+
if (!KEBAB.test(slug)) throw new Error(`"${slug}" is not a kebab-case slug`);
|
|
318
|
+
const dir = resolve2(root, config.plans);
|
|
319
|
+
mkdirSync(dir, { recursive: true });
|
|
320
|
+
const taken = planFilesIn(dir).find((file) => PLAN_FILE.exec(basename(file))?.[2] === slug);
|
|
321
|
+
if (taken !== void 0) {
|
|
322
|
+
throw new Error(`${relative(root, taken).replaceAll("\\", "/")} already has this slug`);
|
|
323
|
+
}
|
|
324
|
+
const number = nextNumber(dir);
|
|
325
|
+
const named = `${number}-${slug}.md`;
|
|
326
|
+
const body = [
|
|
327
|
+
`# Plan ${number}: ${slug.replaceAll("-", " ")}`,
|
|
328
|
+
"",
|
|
329
|
+
"> **Status**: DRAFT \xB7 **Milestone**: \u2014",
|
|
330
|
+
"",
|
|
331
|
+
"## Acceptance criteria",
|
|
332
|
+
"",
|
|
333
|
+
"<!-- Replace this line with EARS criteria. `plan check` refuses until you do. -->",
|
|
334
|
+
"",
|
|
335
|
+
"## Verification",
|
|
336
|
+
"",
|
|
337
|
+
"<!-- The commands that decide it, and what their output has to be. -->",
|
|
338
|
+
""
|
|
339
|
+
].join("\n");
|
|
340
|
+
writeFileSync(join(dir, named), body);
|
|
341
|
+
return `${relative(root, dir).replaceAll("\\", "/")}/${named}`;
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
// src/rows.ts
|
|
345
|
+
import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3 } from "fs";
|
|
346
|
+
import { dirname, resolve as resolve3 } from "path";
|
|
347
|
+
var isoDate = (now) => now.toISOString().slice(0, 10);
|
|
348
|
+
var cell = (text) => text.replaceAll("|", "\\|").replaceAll("\n", " ").trim();
|
|
349
|
+
var appendRow = (file, root, header, cells) => {
|
|
350
|
+
const at = resolve3(root, file);
|
|
351
|
+
mkdirSync2(dirname(at), { recursive: true });
|
|
352
|
+
const row = `| ${cells.map((one) => cell(one)).join(" | ")} |
|
|
353
|
+
`;
|
|
354
|
+
if (!existsSync3(at)) {
|
|
355
|
+
const divider = header.map(() => "---").join(" | ");
|
|
356
|
+
appendFileSync(at, `| ${header.join(" | ")} |
|
|
357
|
+
| ${divider} |
|
|
358
|
+
${row}`);
|
|
359
|
+
return row;
|
|
360
|
+
}
|
|
361
|
+
const body = readFileSync3(at, "utf8");
|
|
362
|
+
appendFileSync(at, body.endsWith("\n") ? row : `
|
|
363
|
+
${row}`);
|
|
364
|
+
return row;
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
// src/journal.ts
|
|
368
|
+
var FALLBACK_KINDS = ["stub", "mock", "guess", "skip"];
|
|
369
|
+
var JOURNAL_HEADER = ["date", "kind", "where", "why"];
|
|
370
|
+
var WHERE = /^\S+:\d+$/;
|
|
371
|
+
var appendFallback = ({ config, kind, now, root, where, why }) => {
|
|
372
|
+
if (!FALLBACK_KINDS.includes(kind)) {
|
|
373
|
+
throw new Error(`"${kind}" is not a fallback kind \u2014 one of ${FALLBACK_KINDS.join(", ")}`);
|
|
374
|
+
}
|
|
375
|
+
if (!WHERE.test(where.trim())) throw new Error(`"${where}" is not a path:line`);
|
|
376
|
+
if (why.trim() === "") throw new Error("why is required \u2014 an unexplained fallback is invisible");
|
|
377
|
+
return appendRow(config.journal, root, JOURNAL_HEADER, [isoDate(now), kind, where.trim(), why]);
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
// src/git.ts
|
|
381
|
+
import { spawnSync } from "child_process";
|
|
382
|
+
var git = (args, cwd) => {
|
|
383
|
+
const done = spawnSync("git", args, { cwd, encoding: "utf8" });
|
|
384
|
+
if (done.error !== void 0) throw done.error;
|
|
385
|
+
return { code: done.status ?? -1, stderr: done.stderr ?? "", stdout: done.stdout ?? "" };
|
|
386
|
+
};
|
|
387
|
+
var lines = (out) => out.split("\n").map((line) => line.trim()).filter((line) => line !== "");
|
|
388
|
+
var commitHash = (ref, cwd) => {
|
|
389
|
+
const done = git(["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], cwd);
|
|
390
|
+
return done.code === 0 ? done.stdout.trim() : void 0;
|
|
391
|
+
};
|
|
392
|
+
var filesChangedBy = (hash, cwd) => {
|
|
393
|
+
const parent = git(["rev-parse", "--verify", "--quiet", `${hash}~1^{commit}`], cwd);
|
|
394
|
+
if (parent.code === 0) return lines(git(["diff", "--name-only", `${hash}~1`, hash], cwd).stdout);
|
|
395
|
+
return lines(
|
|
396
|
+
git(["diff-tree", "--root", "--no-commit-id", "--name-only", "-r", hash], cwd).stdout
|
|
397
|
+
);
|
|
398
|
+
};
|
|
399
|
+
var patchOf = (hash, cwd) => {
|
|
400
|
+
const parent = git(["rev-parse", "--verify", "--quiet", `${hash}~1^{commit}`], cwd);
|
|
401
|
+
if (parent.code === 0) {
|
|
402
|
+
return git(["diff", "-U0", "--no-color", `${hash}~1`, hash], cwd);
|
|
403
|
+
}
|
|
404
|
+
return git(["diff-tree", "--root", "-p", "-U0", "--no-color", "-r", hash], cwd);
|
|
405
|
+
};
|
|
406
|
+
var stagedPatch = (cwd) => git(["diff", "--cached", "-U0", "--no-color"], cwd);
|
|
407
|
+
var porcelain = (cwd) => git(["status", "--porcelain"], cwd).stdout.split("\n").filter((line) => line.trim() !== "");
|
|
408
|
+
var headOf = (cwd) => {
|
|
409
|
+
const done = git(["log", "-1", "--format=%H%n%s"], cwd);
|
|
410
|
+
if (done.code !== 0) return void 0;
|
|
411
|
+
const [hash, subject] = done.stdout.split("\n");
|
|
412
|
+
if (hash === void 0 || hash.trim() === "") return void 0;
|
|
413
|
+
return { hash: hash.trim(), subject: (subject ?? "").trim() };
|
|
414
|
+
};
|
|
415
|
+
var messageOf = (ref, cwd) => {
|
|
416
|
+
const done = git(["log", "-1", "--format=%B", ref], cwd);
|
|
417
|
+
return done.code === 0 ? done.stdout : void 0;
|
|
418
|
+
};
|
|
419
|
+
var isRepo = (cwd) => git(["rev-parse", "--is-inside-work-tree"], cwd).stdout.trim() === "true";
|
|
420
|
+
|
|
421
|
+
// src/proof.ts
|
|
422
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
423
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readdirSync as readdirSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
424
|
+
import { join as join2, resolve as resolve4 } from "path";
|
|
425
|
+
var KEBAB2 = /^[a-z\d][a-z\d-]*$/;
|
|
426
|
+
var NUMBERED = /^(\d{3,})-/;
|
|
427
|
+
var nextNumber2 = (dir) => {
|
|
428
|
+
const highest = existsSync4(dir) ? readdirSync2(dir).reduce(
|
|
429
|
+
(most, name) => Math.max(most, Number(NUMBERED.exec(name)?.[1] ?? 0)),
|
|
430
|
+
0
|
|
431
|
+
) : 0;
|
|
432
|
+
return String(highest + 1).padStart(3, "0");
|
|
433
|
+
};
|
|
434
|
+
var bound = (output, maxLines) => {
|
|
435
|
+
const lines2 = output.split("\n");
|
|
436
|
+
if (lines2.length <= maxLines) return output;
|
|
437
|
+
const dropped = lines2.length - maxLines;
|
|
438
|
+
return `${lines2.slice(0, maxLines).join("\n")}
|
|
439
|
+
\u2026 ${dropped} more line${dropped === 1 ? "" : "s"}`;
|
|
440
|
+
};
|
|
441
|
+
var captureProof = ({ command, config, root, slug, url }) => {
|
|
442
|
+
if (!KEBAB2.test(slug)) throw new Error(`"${slug}" is not a kebab-case slug`);
|
|
443
|
+
const dir = resolve4(root, config.proofs);
|
|
444
|
+
mkdirSync3(dir, { recursive: true });
|
|
445
|
+
const file = `${nextNumber2(dir)}-${slug}.md`;
|
|
446
|
+
const done = spawnSync2(command, { cwd: root, encoding: "utf8", shell: true });
|
|
447
|
+
const exitCode = done.status ?? -1;
|
|
448
|
+
const head = headOf(root);
|
|
449
|
+
const output = `${done.stdout ?? ""}${done.stderr ?? ""}`;
|
|
450
|
+
const body = [
|
|
451
|
+
`# proof \u2014 ${slug}`,
|
|
452
|
+
"",
|
|
453
|
+
`- **HEAD**: \`${head?.hash ?? "no commit"}\` \u2014 ${head?.subject ?? "\u2014"}`,
|
|
454
|
+
`- **Command**: \`${command}\``,
|
|
455
|
+
...url === void 0 ? [] : [`- **URL**: ${url}`],
|
|
456
|
+
`- **Result**: exit ${exitCode}`,
|
|
457
|
+
"",
|
|
458
|
+
"```",
|
|
459
|
+
bound(output.replace(/\n$/, ""), config.maxLines),
|
|
460
|
+
"```",
|
|
461
|
+
""
|
|
462
|
+
].join("\n");
|
|
463
|
+
writeFileSync2(join2(dir, file), body);
|
|
464
|
+
return { exitCode, file: `${config.proofs}/${file}` };
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
// src/delivery.ts
|
|
468
|
+
var SCISSORS = "# ------------------------ >8 ------------------------";
|
|
469
|
+
var TRAILER = /^[A-Za-z][\dA-Za-z-]*: \S/;
|
|
470
|
+
var countedLines = (message) => {
|
|
471
|
+
const cut = message.indexOf(SCISSORS);
|
|
472
|
+
const kept = (cut === -1 ? message : message.slice(0, cut)).split("\n").filter((line) => !line.startsWith("#")).map((line) => line.trimEnd());
|
|
473
|
+
while (kept.length > 0 && (kept.at(-1) ?? "").trim() === "") kept.pop();
|
|
474
|
+
while (kept.length > 1 && TRAILER.test(kept.at(-1) ?? "")) kept.pop();
|
|
475
|
+
return kept.filter((line) => line.trim() !== "");
|
|
476
|
+
};
|
|
477
|
+
var checkCommitMessage = (message, config) => {
|
|
478
|
+
const lines2 = countedLines(message);
|
|
479
|
+
if (lines2.length === 0) return { refusal: "the commit message is empty" };
|
|
480
|
+
if (lines2.length <= config.commitMessageLines) return {};
|
|
481
|
+
return {
|
|
482
|
+
refusal: `the commit message is ${lines2.length} line(s); ledger.commitMessageLines is ${config.commitMessageLines} (subject + ${config.commitMessageLines - 1} body). Put the rest in a proof file, or split the commit.`
|
|
483
|
+
};
|
|
484
|
+
};
|
|
485
|
+
var COMMENTS = [
|
|
486
|
+
{
|
|
487
|
+
comments: { block: ["/*", "*/", "*", "{/*", "*/}"], line: ["//"] },
|
|
488
|
+
extensions: [
|
|
489
|
+
"c",
|
|
490
|
+
"cc",
|
|
491
|
+
"cpp",
|
|
492
|
+
"cjs",
|
|
493
|
+
"cts",
|
|
494
|
+
"go",
|
|
495
|
+
"h",
|
|
496
|
+
"java",
|
|
497
|
+
"js",
|
|
498
|
+
"jsx",
|
|
499
|
+
"kt",
|
|
500
|
+
"less",
|
|
501
|
+
"mjs",
|
|
502
|
+
"mts",
|
|
503
|
+
"rs",
|
|
504
|
+
"scss",
|
|
505
|
+
"swift",
|
|
506
|
+
"ts",
|
|
507
|
+
"tsx"
|
|
508
|
+
]
|
|
509
|
+
},
|
|
510
|
+
{ comments: { block: ["/*", "*/", "*"], line: [] }, extensions: ["css"] },
|
|
511
|
+
{
|
|
512
|
+
comments: { block: [], line: ["#"] },
|
|
513
|
+
extensions: [
|
|
514
|
+
"bash",
|
|
515
|
+
"cfg",
|
|
516
|
+
"conf",
|
|
517
|
+
"env",
|
|
518
|
+
"gitignore",
|
|
519
|
+
"ini",
|
|
520
|
+
"mk",
|
|
521
|
+
"properties",
|
|
522
|
+
"py",
|
|
523
|
+
"rb",
|
|
524
|
+
"sh",
|
|
525
|
+
"toml",
|
|
526
|
+
"zsh"
|
|
527
|
+
]
|
|
528
|
+
},
|
|
529
|
+
{ comments: { block: [], line: ["#"] }, extensions: ["yaml", "yml"] },
|
|
530
|
+
{
|
|
531
|
+
comments: { block: ["<!--", "-->"], line: [] },
|
|
532
|
+
extensions: ["astro", "html", "md", "mdx", "svelte", "svg", "vue", "xml"]
|
|
533
|
+
},
|
|
534
|
+
{ comments: { block: ["/*", "*/", "*"], line: ["--"] }, extensions: ["sql"] }
|
|
535
|
+
];
|
|
536
|
+
var BY_NAME = {
|
|
537
|
+
dockerfile: { block: [], line: ["#"] },
|
|
538
|
+
makefile: { block: [], line: ["#"] }
|
|
539
|
+
};
|
|
540
|
+
var commentsFor = (file) => {
|
|
541
|
+
const name = (file.split("/").at(-1) ?? "").toLowerCase();
|
|
542
|
+
const byName = BY_NAME[name];
|
|
543
|
+
if (byName !== void 0) return byName;
|
|
544
|
+
const extension = name.includes(".") ? name.split(".").at(-1) ?? "" : "";
|
|
545
|
+
return COMMENTS.find((one) => one.extensions.includes(extension))?.comments;
|
|
546
|
+
};
|
|
547
|
+
var isCommentary = (line, comments) => {
|
|
548
|
+
const text = line.trim();
|
|
549
|
+
if (text === "") return true;
|
|
550
|
+
if (comments === void 0) return false;
|
|
551
|
+
return [...comments.line, ...comments.block].some((token) => text.startsWith(token));
|
|
552
|
+
};
|
|
553
|
+
var FILE_LINE = /^\+\+\+ b\/(.*)$/;
|
|
554
|
+
var checkDelivery = ({ commit, root, staged }) => {
|
|
555
|
+
const done = staged ? stagedPatch(root) : patchOf(commit ?? "HEAD", root);
|
|
556
|
+
if (done.code !== 0) return { refusal: `the diff could not be read: ${done.stderr.trim()}` };
|
|
557
|
+
let file = "";
|
|
558
|
+
let comments;
|
|
559
|
+
let changed = 0;
|
|
560
|
+
const commentary = [];
|
|
561
|
+
const code = [];
|
|
562
|
+
for (const line of done.stdout.split("\n")) {
|
|
563
|
+
const named = FILE_LINE.exec(line);
|
|
564
|
+
if (named?.[1] !== void 0) {
|
|
565
|
+
file = named[1];
|
|
566
|
+
comments = commentsFor(file);
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
if (line.startsWith("+++") || line.startsWith("---") || line.startsWith("@@")) continue;
|
|
570
|
+
if (!line.startsWith("+") && !line.startsWith("-")) continue;
|
|
571
|
+
changed += 1;
|
|
572
|
+
const body = line.slice(1);
|
|
573
|
+
if (isCommentary(body, comments)) commentary.push(file);
|
|
574
|
+
else code.push(file);
|
|
575
|
+
}
|
|
576
|
+
if (changed === 0) return { refusal: "the diff changes nothing at all" };
|
|
577
|
+
if (code.length > 0) return {};
|
|
578
|
+
return {
|
|
579
|
+
refusal: `every changed line is a comment or whitespace \u2014 that is not a delivery. Files: ${[...new Set(commentary)].join(", ")}`
|
|
580
|
+
};
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
// src/tick.ts
|
|
584
|
+
import { existsSync as existsSync5, readdirSync as readdirSync3, readFileSync as readFileSync4 } from "fs";
|
|
585
|
+
import { resolve as resolve5 } from "path";
|
|
586
|
+
var PROGRESS_HEADER = ["tick", "date", "plan", "commit", "proof", "note"];
|
|
587
|
+
var planFileFor = (number, dir) => {
|
|
588
|
+
if (!existsSync5(dir)) return void 0;
|
|
589
|
+
const padded = number.padStart(3, "0");
|
|
590
|
+
return readdirSync3(dir).find((name) => name.startsWith(`${padded}-`) && name.endsWith(".md"));
|
|
591
|
+
};
|
|
592
|
+
var nextTick = (file) => {
|
|
593
|
+
if (!existsSync5(file)) return 1;
|
|
594
|
+
return readFileSync4(file, "utf8").split("\n").reduce((most, line) => {
|
|
595
|
+
const first = /^\| *(\d+) *\|/.exec(line)?.[1];
|
|
596
|
+
return first === void 0 ? most : Math.max(most, Number(first) + 1);
|
|
597
|
+
}, 1);
|
|
598
|
+
};
|
|
599
|
+
var runTick = ({
|
|
600
|
+
commit,
|
|
601
|
+
config,
|
|
602
|
+
note,
|
|
603
|
+
now,
|
|
604
|
+
plan,
|
|
605
|
+
proof,
|
|
606
|
+
root
|
|
607
|
+
}) => {
|
|
608
|
+
if (!isRepo(root)) throw new Error(`${root} is not a git repository`);
|
|
609
|
+
const hash = commitHash(commit, root);
|
|
610
|
+
if (hash === void 0) return { refusal: `${commit} is not a commit` };
|
|
611
|
+
const plansDir = resolve5(root, config.plans);
|
|
612
|
+
const planFile = planFileFor(plan, plansDir);
|
|
613
|
+
if (planFile === void 0) return { refusal: `no plan ${plan} in ${config.plans}` };
|
|
614
|
+
const report = checkPlans({ config, root, target: `${config.plans}/${planFile}` });
|
|
615
|
+
if (report.violations.length > 0) {
|
|
616
|
+
return { refusal: `plan ${plan} does not pass plan check:
|
|
617
|
+
${report.violations.join("\n")}` };
|
|
618
|
+
}
|
|
619
|
+
if (!existsSync5(resolve5(root, proof))) return { refusal: `${proof} does not exist` };
|
|
620
|
+
const changed = filesChangedBy(hash, root);
|
|
621
|
+
const owned = ledgerOwned(config);
|
|
622
|
+
const runtime = changed.filter(
|
|
623
|
+
(file) => !owned.some((one) => file === one || file.startsWith(`${one.replace(/\/$/, "")}/`)) && matchesRuntime(file, config.runtime)
|
|
624
|
+
);
|
|
625
|
+
if (runtime.length === 0) {
|
|
626
|
+
return {
|
|
627
|
+
refusal: `${hash.slice(0, 8)} ships no runtime code (G0). Its diff:
|
|
628
|
+
${changed.map((file) => ` ${file}`).join("\n")}`
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
const written = checkCommitMessage(messageOf(hash, root) ?? "", config);
|
|
632
|
+
if (written.refusal !== void 0) return { refusal: written.refusal };
|
|
633
|
+
const delivered = checkDelivery({ commit: hash, root, staged: false });
|
|
634
|
+
if (delivered.refusal !== void 0) return { refusal: delivered.refusal };
|
|
635
|
+
const number = nextTick(resolve5(root, config.progress));
|
|
636
|
+
const row = appendRow(config.progress, root, PROGRESS_HEADER, [
|
|
637
|
+
String(number),
|
|
638
|
+
isoDate(now),
|
|
639
|
+
plan,
|
|
640
|
+
hash.slice(0, 8),
|
|
641
|
+
proof,
|
|
642
|
+
note ?? "\u2014"
|
|
643
|
+
]);
|
|
644
|
+
return { number, row };
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
// src/decide.ts
|
|
648
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
649
|
+
import { resolve as resolve6 } from "path";
|
|
650
|
+
var REVISIT_COLUMN = "Revisit trigger";
|
|
651
|
+
var ID = /^D-\d{3,}$/;
|
|
652
|
+
var FOR = [
|
|
653
|
+
{ key: "id", prefix: "id" },
|
|
654
|
+
{ key: "text", prefix: "decision" },
|
|
655
|
+
{ key: "status", prefix: "status" },
|
|
656
|
+
{ key: "why", prefix: "why" },
|
|
657
|
+
{ key: "evidence", prefix: "evidence" },
|
|
658
|
+
{ key: "revisit", prefix: "revisit" }
|
|
659
|
+
];
|
|
660
|
+
var normal = (heading) => heading.toLowerCase().replaceAll(/[^a-z]/g, "");
|
|
661
|
+
var runDecide = ({
|
|
662
|
+
config,
|
|
663
|
+
evidence,
|
|
664
|
+
id,
|
|
665
|
+
revisit,
|
|
666
|
+
root,
|
|
667
|
+
status,
|
|
668
|
+
text,
|
|
669
|
+
why
|
|
670
|
+
}) => {
|
|
671
|
+
if (!ID.test(id.trim())) return { refusal: `"${id}" is not an id of the form D-NNN` };
|
|
672
|
+
if (revisit.trim() === "") {
|
|
673
|
+
return {
|
|
674
|
+
refusal: "a decision needs a revisit trigger \u2014 the event that reopens it. A register without one is a diary, not an instrument (C10)."
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
const at = resolve6(root, config.decisions);
|
|
678
|
+
if (!existsSync6(at)) return { refusal: `${config.decisions} does not exist` };
|
|
679
|
+
const body = readFileSync5(at, "utf8");
|
|
680
|
+
const lines2 = body.split("\n");
|
|
681
|
+
const table = firstTable(lines2);
|
|
682
|
+
if (table === void 0) return { refusal: `${config.decisions} has no table in it` };
|
|
683
|
+
const columns = table.header.map((heading) => normal(heading));
|
|
684
|
+
if (!columns.some((heading) => heading.startsWith("revisit"))) {
|
|
685
|
+
return {
|
|
686
|
+
refusal: `${config.decisions} has no "${REVISIT_COLUMN}" column (C10). Its header has to become:
|
|
687
|
+
${renderRow([...table.header, REVISIT_COLUMN])}`
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
const taken = table.rows.find((index) => rowCells(lines2[index] ?? "")[0]?.trim() === id.trim());
|
|
691
|
+
if (taken !== void 0) return { refusal: `${config.decisions} already carries ${id}` };
|
|
692
|
+
const values = {
|
|
693
|
+
evidence,
|
|
694
|
+
id: id.trim(),
|
|
695
|
+
revisit,
|
|
696
|
+
status: status ?? "\u2014",
|
|
697
|
+
text,
|
|
698
|
+
why
|
|
699
|
+
};
|
|
700
|
+
const cells = columns.map((heading) => {
|
|
701
|
+
const found = FOR.find((one) => heading.startsWith(one.prefix));
|
|
702
|
+
return cell(found === void 0 ? "\u2014" : values[found.key] ?? "\u2014");
|
|
703
|
+
});
|
|
704
|
+
const row = renderRow(cells);
|
|
705
|
+
const last = table.rows.at(-1) ?? table.headerIndex + 1;
|
|
706
|
+
writeFileSync3(at, [...lines2.slice(0, last + 1), row, ...lines2.slice(last + 1)].join("\n"));
|
|
707
|
+
return { row };
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
// src/gate-report.ts
|
|
711
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
|
|
712
|
+
import { resolve as resolve7 } from "path";
|
|
713
|
+
var GATE_REPORT = ".geonosis/gate-report.json";
|
|
714
|
+
var isRecord2 = (value) => typeof value === "object" && value !== null;
|
|
715
|
+
var gateSummary = (root) => {
|
|
716
|
+
const at = resolve7(root, GATE_REPORT);
|
|
717
|
+
if (!existsSync7(at)) return void 0;
|
|
718
|
+
let parsed;
|
|
719
|
+
try {
|
|
720
|
+
parsed = JSON.parse(readFileSync6(at, "utf8"));
|
|
721
|
+
} catch {
|
|
722
|
+
return void 0;
|
|
723
|
+
}
|
|
724
|
+
if (!isRecord2(parsed)) return void 0;
|
|
725
|
+
return {
|
|
726
|
+
finishedAt: typeof parsed["finishedAt"] === "string" ? parsed["finishedAt"] : "\u2014",
|
|
727
|
+
ok: parsed["ok"] === true,
|
|
728
|
+
tier: typeof parsed["tier"] === "string" ? parsed["tier"] : "\u2014"
|
|
729
|
+
};
|
|
730
|
+
};
|
|
731
|
+
var BASELINE_DEFAULT = "gate-baseline.json";
|
|
732
|
+
var stringAt = (value, key) => {
|
|
733
|
+
if (!isRecord2(value)) return void 0;
|
|
734
|
+
const found = value[key];
|
|
735
|
+
return typeof found === "string" ? found : void 0;
|
|
736
|
+
};
|
|
737
|
+
var baselineKeyCount = (root) => {
|
|
738
|
+
const named = (() => {
|
|
739
|
+
const config = resolve7(root, "geonosis.json");
|
|
740
|
+
if (existsSync7(config)) {
|
|
741
|
+
try {
|
|
742
|
+
const ratchet = JSON.parse(readFileSync6(config, "utf8"))["ratchet"];
|
|
743
|
+
const found = stringAt(ratchet, "baseline");
|
|
744
|
+
if (found !== void 0) return found;
|
|
745
|
+
} catch {
|
|
746
|
+
return BASELINE_DEFAULT;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
const own = resolve7(root, "geonosis.ratchet.json");
|
|
750
|
+
if (existsSync7(own)) {
|
|
751
|
+
try {
|
|
752
|
+
return stringAt(JSON.parse(readFileSync6(own, "utf8")), "baseline") ?? BASELINE_DEFAULT;
|
|
753
|
+
} catch {
|
|
754
|
+
return BASELINE_DEFAULT;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
return BASELINE_DEFAULT;
|
|
758
|
+
})();
|
|
759
|
+
const at = resolve7(root, named);
|
|
760
|
+
if (!existsSync7(at)) return void 0;
|
|
761
|
+
try {
|
|
762
|
+
const parsed = JSON.parse(readFileSync6(at, "utf8"));
|
|
763
|
+
return isRecord2(parsed) ? Object.keys(parsed).length : void 0;
|
|
764
|
+
} catch {
|
|
765
|
+
return void 0;
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
var lastRow = (file, root) => {
|
|
769
|
+
const at = resolve7(root, file);
|
|
770
|
+
if (!existsSync7(at)) return void 0;
|
|
771
|
+
return readFileSync6(at, "utf8").split("\n").filter((line) => /^\| *\d+ *\|/.test(line)).at(-1);
|
|
772
|
+
};
|
|
773
|
+
var rowCount = (file, root) => {
|
|
774
|
+
const at = resolve7(root, file);
|
|
775
|
+
if (!existsSync7(at)) return 0;
|
|
776
|
+
return readFileSync6(at, "utf8").split("\n").filter((line) => /^\| \d{4}-\d{2}-\d{2} \|/.test(line)).length;
|
|
777
|
+
};
|
|
778
|
+
|
|
779
|
+
// src/handoff.ts
|
|
780
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
781
|
+
import { resolve as resolve8 } from "path";
|
|
782
|
+
var CLOSE_START = "<!-- geonosis-ledger:session-close -->";
|
|
783
|
+
var CLOSE_END = "<!-- /geonosis-ledger:session-close -->";
|
|
784
|
+
var DONE = /* @__PURE__ */ new Set(["DONE", "SHIPPED", "CLOSED", "ABANDONED"]);
|
|
785
|
+
var bullets = (list, empty) => list.length === 0 ? [`- ${empty}`] : list.map((one) => `- ${one}`);
|
|
786
|
+
var runHandoff = ({ config, root, write }) => {
|
|
787
|
+
const head = headOf(root);
|
|
788
|
+
const dirty = porcelain(root).filter((line) => !line.endsWith(` ${config.handoff}`));
|
|
789
|
+
const gate = gateSummary(root);
|
|
790
|
+
const tick = lastRow(config.progress, root);
|
|
791
|
+
const open = planStatuses({ config, root }).filter((plan) => !DONE.has(plan.status.toUpperCase()));
|
|
792
|
+
const block = [
|
|
793
|
+
CLOSE_START,
|
|
794
|
+
"## SESSION CLOSE",
|
|
795
|
+
"",
|
|
796
|
+
"**HEAD**",
|
|
797
|
+
...bullets(head === void 0 ? [] : [`\`${head.hash}\` \u2014 ${head.subject}`], "no commit yet"),
|
|
798
|
+
"",
|
|
799
|
+
"**Uncommitted**",
|
|
800
|
+
...bullets(dirty, "nothing uncommitted"),
|
|
801
|
+
"",
|
|
802
|
+
"**Last tick**",
|
|
803
|
+
...bullets(tick === void 0 ? [] : [tick], "no tick recorded"),
|
|
804
|
+
"",
|
|
805
|
+
"**Last gate report**",
|
|
806
|
+
...bullets(
|
|
807
|
+
gate === void 0 ? [] : [`${gate.tier} \u2014 ${gate.ok ? "ok" : "FAILED"} at ${gate.finishedAt}`],
|
|
808
|
+
"no gate report"
|
|
809
|
+
),
|
|
810
|
+
"",
|
|
811
|
+
"**Open plans**",
|
|
812
|
+
...bullets(
|
|
813
|
+
open.map((plan) => `${plan.file} \u2014 ${plan.status}`),
|
|
814
|
+
"no open plans"
|
|
815
|
+
),
|
|
816
|
+
"",
|
|
817
|
+
CLOSE_END
|
|
818
|
+
].join("\n");
|
|
819
|
+
if (!write) return { block, written: false };
|
|
820
|
+
const at = resolve8(root, config.handoff);
|
|
821
|
+
const body = existsSync8(at) ? readFileSync7(at, "utf8") : "";
|
|
822
|
+
const from = body.indexOf(CLOSE_START);
|
|
823
|
+
const to = body.indexOf(CLOSE_END);
|
|
824
|
+
const next = from === -1 || to === -1 ? `${body.endsWith("\n") || body === "" ? body : `${body}
|
|
825
|
+
`}
|
|
826
|
+
${block}
|
|
827
|
+
` : `${body.slice(0, from)}${block}${body.slice(to + CLOSE_END.length)}`;
|
|
828
|
+
writeFileSync4(at, next);
|
|
829
|
+
return { block, written: true };
|
|
830
|
+
};
|
|
831
|
+
|
|
832
|
+
// src/status.ts
|
|
833
|
+
var STATUS_MAX_LINES = 6;
|
|
834
|
+
var readStatus = ({ config, root }) => {
|
|
835
|
+
const counts = /* @__PURE__ */ new Map();
|
|
836
|
+
for (const plan of planStatuses({ config, root })) {
|
|
837
|
+
counts.set(plan.status, (counts.get(plan.status) ?? 0) + 1);
|
|
838
|
+
}
|
|
839
|
+
const head = headOf(root);
|
|
840
|
+
const gate = gateSummary(root);
|
|
841
|
+
const tick = lastRow(config.progress, root);
|
|
842
|
+
const keys = baselineKeyCount(root);
|
|
843
|
+
return {
|
|
844
|
+
...keys === void 0 ? {} : { baselineKeys: keys },
|
|
845
|
+
...gate === void 0 ? {} : { gate },
|
|
846
|
+
...head === void 0 ? {} : { head: head.subject },
|
|
847
|
+
journalRows: rowCount(config.journal, root),
|
|
848
|
+
...tick === void 0 ? {} : { lastTick: tick },
|
|
849
|
+
plans: [...counts].map(([status, count]) => ({ count, status })).toSorted((a, b) => a.status.localeCompare(b.status))
|
|
850
|
+
};
|
|
851
|
+
};
|
|
852
|
+
var label = (text) => text.padEnd(9, " ");
|
|
853
|
+
var formatStatus = (digest) => [
|
|
854
|
+
`${label("HEAD")} ${digest.head ?? "no commit yet"}`,
|
|
855
|
+
`${label("plans")} ${digest.plans.length === 0 ? "no plans" : digest.plans.map((one) => `${one.status} ${one.count}`).join(" \xB7 ")}`,
|
|
856
|
+
`${label("last tick")} ${digest.lastTick ?? "no tick yet"}`,
|
|
857
|
+
`${label("gate")} ${digest.gate === void 0 ? "no gate report" : `${digest.gate.tier} \u2014 ${digest.gate.ok ? "ok" : "FAILED"} at ${digest.gate.finishedAt}`}`,
|
|
858
|
+
`${label("baseline")} ${digest.baselineKeys === void 0 ? "no baseline" : `${digest.baselineKeys} counter(s)`}`,
|
|
859
|
+
`${label("journal")} ${digest.journalRows} fallback(s)`
|
|
860
|
+
].join("\n");
|
|
861
|
+
|
|
862
|
+
// src/architecture.ts
|
|
863
|
+
import { existsSync as existsSync9, readdirSync as readdirSync4, readFileSync as readFileSync8, statSync as statSync2 } from "fs";
|
|
864
|
+
import { join as join3, resolve as resolve9 } from "path";
|
|
865
|
+
var EDGES_HEADER = "| tag | packages | may depend on |";
|
|
866
|
+
var normal2 = (heading) => heading.toLowerCase().replaceAll(/[^a-z]/g, "");
|
|
867
|
+
var WANTED = ["tag", "packages", "maydependon"];
|
|
868
|
+
var listOf = (cell2) => cell2.split(",").map((one) => plainCell(one)).filter((one) => one !== "" && one !== "\u2014" && one !== "-");
|
|
869
|
+
var readEdges = (lines2) => {
|
|
870
|
+
const bodies = [lines2, ...headingsOf(lines2).map((heading) => lines2.slice(heading.index))];
|
|
871
|
+
for (const body of bodies) {
|
|
872
|
+
const table = firstTable(body);
|
|
873
|
+
if (table === void 0) continue;
|
|
874
|
+
const columns = table.header.map((one) => normal2(one));
|
|
875
|
+
if (columns.length < 3 || !WANTED.every((wanted, at) => columns[at] === wanted)) continue;
|
|
876
|
+
return table.rows.map((index) => {
|
|
877
|
+
const cells = rowCells(body[index] ?? "");
|
|
878
|
+
const notes = [];
|
|
879
|
+
const mayDependOn = (cells[2] ?? "").split(",").map((one) => plainCell(one)).filter((one) => one !== "" && one !== "\u2014" && one !== "-").map((one) => {
|
|
880
|
+
const annotated = /^(.*?)\s*\((.+)\)$/.exec(one);
|
|
881
|
+
if (annotated?.[1] === void 0 || annotated[2] === void 0) return one;
|
|
882
|
+
notes.push(`may depend on ${annotated[1]}: ${annotated[2]}`);
|
|
883
|
+
return annotated[1];
|
|
884
|
+
});
|
|
885
|
+
return {
|
|
886
|
+
mayDependOn,
|
|
887
|
+
notes,
|
|
888
|
+
packages: listOf(cells[1] ?? ""),
|
|
889
|
+
tag: plainCell(cells[0] ?? "")
|
|
890
|
+
};
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
return void 0;
|
|
894
|
+
};
|
|
895
|
+
var globsOf = (root) => {
|
|
896
|
+
const pnpm = resolve9(root, "pnpm-workspace.yaml");
|
|
897
|
+
if (existsSync9(pnpm)) {
|
|
898
|
+
const found = [];
|
|
899
|
+
let inside = false;
|
|
900
|
+
for (const line of readFileSync8(pnpm, "utf8").split("\n")) {
|
|
901
|
+
if (line.startsWith("packages:")) {
|
|
902
|
+
inside = true;
|
|
903
|
+
continue;
|
|
904
|
+
}
|
|
905
|
+
if (inside && /^\s*-\s+/.test(line)) {
|
|
906
|
+
found.push(
|
|
907
|
+
line.replace(/^\s*-\s+/, "").replaceAll(/['"]/g, "").trim()
|
|
908
|
+
);
|
|
909
|
+
continue;
|
|
910
|
+
}
|
|
911
|
+
if (inside && line.trim() !== "" && !line.startsWith(" ")) inside = false;
|
|
912
|
+
}
|
|
913
|
+
if (found.length > 0) return found;
|
|
914
|
+
}
|
|
915
|
+
const manifest = resolve9(root, "package.json");
|
|
916
|
+
if (!existsSync9(manifest)) return void 0;
|
|
917
|
+
const parsed = JSON.parse(readFileSync8(manifest, "utf8"));
|
|
918
|
+
const declared = parsed.workspaces;
|
|
919
|
+
if (Array.isArray(declared)) return declared;
|
|
920
|
+
return declared?.packages;
|
|
921
|
+
};
|
|
922
|
+
var workspacePackages = (root) => {
|
|
923
|
+
const globs = globsOf(root);
|
|
924
|
+
if (globs === void 0) return void 0;
|
|
925
|
+
const found = [];
|
|
926
|
+
for (const glob of globs) {
|
|
927
|
+
const dirs = glob.endsWith("/*") ? (() => {
|
|
928
|
+
const parent = resolve9(root, glob.slice(0, -2));
|
|
929
|
+
if (!existsSync9(parent)) return [];
|
|
930
|
+
return readdirSync4(parent).filter((name) => statSync2(join3(parent, name)).isDirectory()).map((name) => `${glob.slice(0, -2)}/${name}`);
|
|
931
|
+
})() : [glob];
|
|
932
|
+
for (const dir of dirs) {
|
|
933
|
+
const manifest = resolve9(root, dir, "package.json");
|
|
934
|
+
if (!existsSync9(manifest)) continue;
|
|
935
|
+
const name = JSON.parse(readFileSync8(manifest, "utf8")).name;
|
|
936
|
+
found.push({ dir, name: name ?? dir });
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
return found;
|
|
940
|
+
};
|
|
941
|
+
var matchPackage = (named, packages) => packages.find(
|
|
942
|
+
(one) => one.dir === named || one.dir.split("/").at(-1) === named || one.name === named || one.name.split("/").at(-1) === named
|
|
943
|
+
);
|
|
944
|
+
var checkArchitecture = ({
|
|
945
|
+
config,
|
|
946
|
+
root,
|
|
947
|
+
workspace
|
|
948
|
+
}) => {
|
|
949
|
+
const at = resolve9(root, config.architecture);
|
|
950
|
+
if (!existsSync9(at)) {
|
|
951
|
+
return { edges: [], notes: [], violations: [`${config.architecture} does not exist`] };
|
|
952
|
+
}
|
|
953
|
+
const edges = readEdges(readFileSync8(at, "utf8").split("\n"));
|
|
954
|
+
if (edges === void 0) {
|
|
955
|
+
return {
|
|
956
|
+
edges: [],
|
|
957
|
+
notes: [],
|
|
958
|
+
violations: [`${config.architecture} has no edges table with the header \`${EDGES_HEADER}\``]
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
const violations = [];
|
|
962
|
+
const notes = [];
|
|
963
|
+
const declared = new Set(edges.map((edge) => edge.tag));
|
|
964
|
+
const seen = /* @__PURE__ */ new Set();
|
|
965
|
+
const packages = workspace ? workspacePackages(root) : void 0;
|
|
966
|
+
if (workspace && packages === void 0) {
|
|
967
|
+
violations.push("no workspace declaration found \u2014 use --no-workspace to check the tags alone");
|
|
968
|
+
}
|
|
969
|
+
for (const edge of edges) {
|
|
970
|
+
if (seen.has(edge.tag)) violations.push(`${edge.tag}: declared twice`);
|
|
971
|
+
seen.add(edge.tag);
|
|
972
|
+
if (packages !== void 0) {
|
|
973
|
+
for (const named of edge.packages) {
|
|
974
|
+
if (matchPackage(named, packages) === void 0) {
|
|
975
|
+
violations.push(`${edge.tag}: "${named}" is not a workspace package`);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
for (const depends of edge.mayDependOn) {
|
|
980
|
+
if (!declared.has(depends)) violations.push(`${edge.tag}: "${depends}" is not a declared tag`);
|
|
981
|
+
}
|
|
982
|
+
notes.push(...edge.notes.map((note) => `${edge.tag}: ${note}`));
|
|
983
|
+
}
|
|
984
|
+
return { edges, notes, ...packages === void 0 ? {} : { packages }, violations };
|
|
985
|
+
};
|
|
986
|
+
|
|
987
|
+
// src/json-splice.ts
|
|
988
|
+
var WHITESPACE = /* @__PURE__ */ new Set([" ", " ", "\n", "\r"]);
|
|
989
|
+
var skipSpace = (source, from) => {
|
|
990
|
+
let at = from;
|
|
991
|
+
while (at < source.length && WHITESPACE.has(source[at] ?? "")) at += 1;
|
|
992
|
+
return at;
|
|
993
|
+
};
|
|
994
|
+
var endOfString = (source, from) => {
|
|
995
|
+
let at = from + 1;
|
|
996
|
+
while (at < source.length) {
|
|
997
|
+
const char = source[at];
|
|
998
|
+
if (char === "\\") {
|
|
999
|
+
at += 2;
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
1002
|
+
if (char === '"') return at + 1;
|
|
1003
|
+
at += 1;
|
|
1004
|
+
}
|
|
1005
|
+
throw new Error("unterminated string");
|
|
1006
|
+
};
|
|
1007
|
+
var endOfValue = (source, from) => {
|
|
1008
|
+
const first = source[from];
|
|
1009
|
+
if (first === '"') return endOfString(source, from);
|
|
1010
|
+
if (first === "{" || first === "[") {
|
|
1011
|
+
const close = first === "{" ? "}" : "]";
|
|
1012
|
+
let depth = 0;
|
|
1013
|
+
let at2 = from;
|
|
1014
|
+
while (at2 < source.length) {
|
|
1015
|
+
const char = source[at2];
|
|
1016
|
+
if (char === '"') {
|
|
1017
|
+
at2 = endOfString(source, at2);
|
|
1018
|
+
continue;
|
|
1019
|
+
}
|
|
1020
|
+
if (char === first) depth += 1;
|
|
1021
|
+
if (char === close) {
|
|
1022
|
+
depth -= 1;
|
|
1023
|
+
if (depth === 0) return at2 + 1;
|
|
1024
|
+
}
|
|
1025
|
+
at2 += 1;
|
|
1026
|
+
}
|
|
1027
|
+
throw new Error("unterminated object or array");
|
|
1028
|
+
}
|
|
1029
|
+
let at = from;
|
|
1030
|
+
while (at < source.length && ![",", "}", "]"].includes(source[at] ?? "")) at += 1;
|
|
1031
|
+
return at;
|
|
1032
|
+
};
|
|
1033
|
+
var elementSpan = (source, from, index) => {
|
|
1034
|
+
let at = from + 1;
|
|
1035
|
+
let seen = 0;
|
|
1036
|
+
while (at < source.length) {
|
|
1037
|
+
at = skipSpace(source, at);
|
|
1038
|
+
if (source[at] === "]") return void 0;
|
|
1039
|
+
if (source[at] === ",") {
|
|
1040
|
+
at += 1;
|
|
1041
|
+
continue;
|
|
1042
|
+
}
|
|
1043
|
+
const end = endOfValue(source, at);
|
|
1044
|
+
if (seen === index) return { end, start: at };
|
|
1045
|
+
seen += 1;
|
|
1046
|
+
at = end;
|
|
1047
|
+
}
|
|
1048
|
+
return void 0;
|
|
1049
|
+
};
|
|
1050
|
+
var memberSpan = (source, from, key) => {
|
|
1051
|
+
if (source[from] === "[") {
|
|
1052
|
+
const index = Number(key);
|
|
1053
|
+
return Number.isInteger(index) ? elementSpan(source, from, index) : void 0;
|
|
1054
|
+
}
|
|
1055
|
+
if (source[from] !== "{") throw new Error("not an object");
|
|
1056
|
+
let at = from + 1;
|
|
1057
|
+
while (at < source.length) {
|
|
1058
|
+
at = skipSpace(source, at);
|
|
1059
|
+
if (source[at] === "}") return void 0;
|
|
1060
|
+
if (source[at] === ",") {
|
|
1061
|
+
at += 1;
|
|
1062
|
+
continue;
|
|
1063
|
+
}
|
|
1064
|
+
if (source[at] !== '"') throw new Error(`unexpected ${source[at] ?? "end"} in object`);
|
|
1065
|
+
const nameEnd = endOfString(source, at);
|
|
1066
|
+
const name = JSON.parse(source.slice(at, nameEnd));
|
|
1067
|
+
const colon = skipSpace(source, nameEnd);
|
|
1068
|
+
if (source[colon] !== ":") throw new Error("expected :");
|
|
1069
|
+
const valueStart = skipSpace(source, colon + 1);
|
|
1070
|
+
const valueEnd = endOfValue(source, valueStart);
|
|
1071
|
+
if (name === key) return { end: valueEnd, start: valueStart };
|
|
1072
|
+
at = valueEnd;
|
|
1073
|
+
}
|
|
1074
|
+
return void 0;
|
|
1075
|
+
};
|
|
1076
|
+
var replaceJsonValue = (source, path, rendered) => {
|
|
1077
|
+
let start = skipSpace(source, 0);
|
|
1078
|
+
let end = endOfValue(source, start);
|
|
1079
|
+
for (const key of path) {
|
|
1080
|
+
const found = memberSpan(source, start, key);
|
|
1081
|
+
if (found === void 0) throw new Error(`"${path.join(".")}": no such key ("${key}")`);
|
|
1082
|
+
start = found.start;
|
|
1083
|
+
end = found.end;
|
|
1084
|
+
}
|
|
1085
|
+
return `${source.slice(0, start)}${rendered}${source.slice(end)}`;
|
|
1086
|
+
};
|
|
1087
|
+
var readJsonValue = (source, path) => {
|
|
1088
|
+
let start = skipSpace(source, 0);
|
|
1089
|
+
let end = endOfValue(source, start);
|
|
1090
|
+
for (const key of path) {
|
|
1091
|
+
const found = memberSpan(source, start, key);
|
|
1092
|
+
if (found === void 0) return void 0;
|
|
1093
|
+
start = found.start;
|
|
1094
|
+
end = found.end;
|
|
1095
|
+
}
|
|
1096
|
+
return JSON.parse(source.slice(start, end));
|
|
1097
|
+
};
|
|
1098
|
+
var render = (value, indent) => JSON.stringify(value, void 0, 2).split("\n").map((line, at) => at === 0 ? line : `${" ".repeat(indent)}${line}`).join("\n");
|
|
1099
|
+
var walk = (source, path) => {
|
|
1100
|
+
let start = skipSpace(source, 0);
|
|
1101
|
+
let end = endOfValue(source, start);
|
|
1102
|
+
for (const [depth, key] of path.entries()) {
|
|
1103
|
+
const opens = source[start];
|
|
1104
|
+
if (opens !== "{" && opens !== "[") {
|
|
1105
|
+
throw new Error(
|
|
1106
|
+
`"${path.join(".")}": "${key}" would sit inside ${source.slice(start, end)}, which is neither an object nor an array`
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
const found = memberSpan(source, start, key);
|
|
1110
|
+
if (found === void 0) return { depth, end, start };
|
|
1111
|
+
start = found.start;
|
|
1112
|
+
end = found.end;
|
|
1113
|
+
}
|
|
1114
|
+
return { depth: path.length, end, start };
|
|
1115
|
+
};
|
|
1116
|
+
var indentAt = (source, at) => {
|
|
1117
|
+
const line = source.slice(source.lastIndexOf("\n", at) + 1, at);
|
|
1118
|
+
return line.length - line.trimStart().length;
|
|
1119
|
+
};
|
|
1120
|
+
var insertMember = (source, span, key, value) => {
|
|
1121
|
+
if (source[span.start] !== "{") {
|
|
1122
|
+
throw new Error(`"${key}" cannot be added to ${source.slice(span.start, span.end)}`);
|
|
1123
|
+
}
|
|
1124
|
+
const between = source.slice(span.start + 1, span.end - 1);
|
|
1125
|
+
const outer = indentAt(source, span.start);
|
|
1126
|
+
const empty = between.trim() === "";
|
|
1127
|
+
const inner = empty ? outer + 2 : indentAt(source, skipSpace(source, span.start + 1));
|
|
1128
|
+
const member = `"${key}": ${render(value, inner)}`;
|
|
1129
|
+
if (empty) {
|
|
1130
|
+
return [
|
|
1131
|
+
source.slice(0, span.start),
|
|
1132
|
+
`{
|
|
1133
|
+
${" ".repeat(inner)}${member}
|
|
1134
|
+
${" ".repeat(outer)}}`,
|
|
1135
|
+
source.slice(span.end)
|
|
1136
|
+
].join("");
|
|
1137
|
+
}
|
|
1138
|
+
const after = span.start + 1 + between.trimEnd().length;
|
|
1139
|
+
return [source.slice(0, after), `,
|
|
1140
|
+
${" ".repeat(inner)}${member}`, source.slice(after)].join("");
|
|
1141
|
+
};
|
|
1142
|
+
var upsertJsonValue = (source, path, value, grow2) => {
|
|
1143
|
+
const found = walk(source, path);
|
|
1144
|
+
if (found.depth === path.length) {
|
|
1145
|
+
return [
|
|
1146
|
+
source.slice(0, found.start),
|
|
1147
|
+
render(value, indentAt(source, found.start)),
|
|
1148
|
+
source.slice(found.end)
|
|
1149
|
+
].join("");
|
|
1150
|
+
}
|
|
1151
|
+
const [key = "", ...under] = path.slice(found.depth);
|
|
1152
|
+
return insertMember(source, found, key, grow2(under, value));
|
|
1153
|
+
};
|
|
1154
|
+
|
|
1155
|
+
// src/sync-docs.ts
|
|
1156
|
+
import { existsSync as existsSync10, readdirSync as readdirSync5, readFileSync as readFileSync9, statSync as statSync3 } from "fs";
|
|
1157
|
+
import { join as join4, resolve as resolve10 } from "path";
|
|
1158
|
+
var DOC_TARGETS = ["agents-md", "cursor"];
|
|
1159
|
+
var isDocTarget = (target) => DOC_TARGETS.includes(target);
|
|
1160
|
+
var frontmatterOf = (source) => /^---\n([\S\s]*?)\n---/.exec(source)?.[1] ?? "";
|
|
1161
|
+
var scalar = (block, key) => new RegExp(`^${key}: *(.*)$`, "m").exec(block)?.[1]?.trim() ?? "";
|
|
1162
|
+
var pathsOf = (source) => {
|
|
1163
|
+
const lines2 = frontmatterOf(source).split("\n");
|
|
1164
|
+
const at = lines2.findIndex((line) => line.trim() === "paths:");
|
|
1165
|
+
if (at === -1) return ["**"];
|
|
1166
|
+
const found = lines2.slice(at + 1).map((line) => /^ *- *"?([^"]+?)"? *$/.exec(line)?.[1]).reduce((all, one) => one === void 0 ? all : [...all, one], []);
|
|
1167
|
+
return found.length === 0 ? ["**"] : found;
|
|
1168
|
+
};
|
|
1169
|
+
var titleOf = (source) => source.split("\n").find((line) => line.startsWith("# "))?.slice(2).trim() ?? "";
|
|
1170
|
+
var bodyOf = (source) => {
|
|
1171
|
+
const block = /^---\n[\S\s]*?\n---\n/.exec(source)?.[0];
|
|
1172
|
+
return (block === void 0 ? source : source.slice(block.length)).replace(/^\n+/, "");
|
|
1173
|
+
};
|
|
1174
|
+
var filesIn = (dir, ending) => existsSync10(dir) && statSync3(dir).isDirectory() ? readdirSync5(dir).filter((name) => name.endsWith(ending) && name !== "README.md").toSorted() : [];
|
|
1175
|
+
var rulesOf = (root, config) => filesIn(resolve10(root, config.rules), ".md").map((name) => {
|
|
1176
|
+
const source = readFileSync9(resolve10(root, config.rules, name), "utf8");
|
|
1177
|
+
return {
|
|
1178
|
+
globs: pathsOf(source),
|
|
1179
|
+
name: name.replace(/\.md$/, ""),
|
|
1180
|
+
source,
|
|
1181
|
+
title: titleOf(source)
|
|
1182
|
+
};
|
|
1183
|
+
});
|
|
1184
|
+
var skillsOf = (root, config) => {
|
|
1185
|
+
const dir = resolve10(root, config.skills);
|
|
1186
|
+
if (!existsSync10(dir) || !statSync3(dir).isDirectory()) return [];
|
|
1187
|
+
return readdirSync5(dir).toSorted().flatMap((entry) => {
|
|
1188
|
+
const at = join4(dir, entry, "SKILL.md");
|
|
1189
|
+
if (!existsSync10(at)) return [];
|
|
1190
|
+
const block = frontmatterOf(readFileSync9(at, "utf8"));
|
|
1191
|
+
return [{ description: scalar(block, "description"), name: scalar(block, "name") || entry }];
|
|
1192
|
+
});
|
|
1193
|
+
};
|
|
1194
|
+
var buildAgentsMd = ({ config, root }) => {
|
|
1195
|
+
const at = resolve10(root, config.law);
|
|
1196
|
+
if (!existsSync10(at)) throw new Error(`${config.law} does not exist \u2014 there is no law to project`);
|
|
1197
|
+
const lines2 = readFileSync9(at, "utf8").split("\n");
|
|
1198
|
+
const laws = sectionUnder(lines2, config.lawSection);
|
|
1199
|
+
if (laws === void 0) {
|
|
1200
|
+
throw new Error(
|
|
1201
|
+
`${config.law} has no section matching ledger.lawSection (${config.lawSection}) \u2014 nothing to put in AGENTS.md`
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
const skills = skillsOf(root, config);
|
|
1205
|
+
const rules = rulesOf(root, config);
|
|
1206
|
+
const parts = [
|
|
1207
|
+
"# AGENTS.md",
|
|
1208
|
+
`**GENERATED \u2014 do not edit.** Written by \`geonosis-ledger sync --target agents-md\` from ${config.law}, ${config.skills}/ and ${config.rules}/. Edit those; this file is their projection, and \`--check\` fails the build when it has drifted.`,
|
|
1209
|
+
"## The laws",
|
|
1210
|
+
laws.join("\n").trim()
|
|
1211
|
+
];
|
|
1212
|
+
if (skills.length > 0) {
|
|
1213
|
+
parts.push(
|
|
1214
|
+
"## Skills \u2014 load the one that matches before you start",
|
|
1215
|
+
skills.map((one) => `- **${one.name}** \u2014 ${one.description}`).join("\n")
|
|
1216
|
+
);
|
|
1217
|
+
}
|
|
1218
|
+
if (rules.length > 0) {
|
|
1219
|
+
parts.push(
|
|
1220
|
+
"## Scoped rules \u2014 what binds where",
|
|
1221
|
+
["| Files | Rule |", "| --- | --- |"].concat(
|
|
1222
|
+
rules.map(
|
|
1223
|
+
(one) => `| ${one.globs.map((glob) => `\`${glob}\``).join(" \xB7 ")} | ${one.title} (\`${config.rules}/${one.name}.md\`) |`
|
|
1224
|
+
)
|
|
1225
|
+
).join("\n")
|
|
1226
|
+
);
|
|
1227
|
+
}
|
|
1228
|
+
return `${parts.join("\n\n")}
|
|
1229
|
+
`;
|
|
1230
|
+
};
|
|
1231
|
+
var buildCursorRules = ({
|
|
1232
|
+
config,
|
|
1233
|
+
root
|
|
1234
|
+
}) => {
|
|
1235
|
+
const rules = rulesOf(root, config);
|
|
1236
|
+
if (rules.length === 0) {
|
|
1237
|
+
throw new Error(`${config.rules}/ holds no rule files \u2014 there is nothing to translate`);
|
|
1238
|
+
}
|
|
1239
|
+
return rules.map((rule) => {
|
|
1240
|
+
const always = rule.globs.length === 1 && rule.globs[0] === "**";
|
|
1241
|
+
const front = [
|
|
1242
|
+
"---",
|
|
1243
|
+
`description: ${rule.title}`,
|
|
1244
|
+
...always ? [] : [`globs: ${rule.globs.join(", ")}`],
|
|
1245
|
+
`alwaysApply: ${String(always)}`,
|
|
1246
|
+
"---"
|
|
1247
|
+
];
|
|
1248
|
+
return {
|
|
1249
|
+
contents: `${front.join("\n")}
|
|
1250
|
+
|
|
1251
|
+
${bodyOf(rule.source)}`,
|
|
1252
|
+
name: `geonosis-${rule.name}.mdc`
|
|
1253
|
+
};
|
|
1254
|
+
});
|
|
1255
|
+
};
|
|
1256
|
+
var buildDoc = ({
|
|
1257
|
+
config,
|
|
1258
|
+
root,
|
|
1259
|
+
target
|
|
1260
|
+
}) => target === "agents-md" ? [{ contents: buildAgentsMd({ config, root }), name: config.agents }] : buildCursorRules({ config, root });
|
|
1261
|
+
|
|
1262
|
+
// src/sync.ts
|
|
1263
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync4, readFileSync as readFileSync10, writeFileSync as writeFileSync5 } from "fs";
|
|
1264
|
+
import { dirname as dirname2, relative as relative2, resolve as resolve11 } from "path";
|
|
1265
|
+
var SYNC_TARGETS = ["turbo", "layer-walls", "restricted-imports", ...DOC_TARGETS];
|
|
1266
|
+
var LAYER_WALLS_RULE = "biological-architecture/layer-walls";
|
|
1267
|
+
var TURBO_PATH = ["boundaries"];
|
|
1268
|
+
var LAYERS_PATH = ["rules", LAYER_WALLS_RULE, "1", "layers"];
|
|
1269
|
+
var RESTRICTED_PATH = ["rules", "no-restricted-imports", "1", "paths"];
|
|
1270
|
+
var turboOf = (edges) => ({
|
|
1271
|
+
tags: Object.fromEntries(
|
|
1272
|
+
edges.toSorted((a, b) => a.tag.localeCompare(b.tag)).map((edge) => [edge.tag, { dependencies: { allow: edge.mayDependOn } }])
|
|
1273
|
+
)
|
|
1274
|
+
});
|
|
1275
|
+
var layersOf = (edges, packages) => edges.map((edge) => ({
|
|
1276
|
+
mayImport: edge.mayDependOn,
|
|
1277
|
+
name: edge.tag,
|
|
1278
|
+
paths: edge.packages.flatMap((named) => {
|
|
1279
|
+
const found = matchPackage(named, packages);
|
|
1280
|
+
return found === void 0 ? [] : [`^${found.name}`, `${found.dir}/`];
|
|
1281
|
+
})
|
|
1282
|
+
}));
|
|
1283
|
+
var restrictedOf = (edges, packages) => {
|
|
1284
|
+
const named = (tag) => (edges.find((edge) => edge.tag === tag)?.packages ?? []).flatMap((one) => {
|
|
1285
|
+
const found = matchPackage(one, packages);
|
|
1286
|
+
return found === void 0 ? [] : [found];
|
|
1287
|
+
});
|
|
1288
|
+
const everything = edges.flatMap((edge) => named(edge.tag));
|
|
1289
|
+
const out = {};
|
|
1290
|
+
for (const edge of edges) {
|
|
1291
|
+
const allowed = new Set(
|
|
1292
|
+
[edge.tag, ...edge.mayDependOn].flatMap((tag) => named(tag)).map((one) => one.name)
|
|
1293
|
+
);
|
|
1294
|
+
for (const one of named(edge.tag)) {
|
|
1295
|
+
out[one.name] = everything.map((other) => other.name).filter((name) => !allowed.has(name));
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
return out;
|
|
1299
|
+
};
|
|
1300
|
+
var packageAt = (file, root, packages) => {
|
|
1301
|
+
const dir = relative2(root, dirname2(resolve11(root, file))).replaceAll("\\", "/");
|
|
1302
|
+
return packages.find((one) => one.dir === dir)?.name;
|
|
1303
|
+
};
|
|
1304
|
+
var grow = (missing, value) => {
|
|
1305
|
+
const [key, ...rest] = missing;
|
|
1306
|
+
if (key === void 0) return value;
|
|
1307
|
+
if (!/^\d+$/.test(key)) return { [key]: grow(rest, value) };
|
|
1308
|
+
if (key !== "1") {
|
|
1309
|
+
throw new Error(`element ${key} of a rule cannot be created \u2014 a rule is ["error", { \u2026 }]`);
|
|
1310
|
+
}
|
|
1311
|
+
return ["error", grow(rest, value)];
|
|
1312
|
+
};
|
|
1313
|
+
var MARKER = (name) => `--- ${name}`;
|
|
1314
|
+
var runDocSync = ({ check, config, root, target, write }) => {
|
|
1315
|
+
if (!isDocTarget(target)) throw new Error(`"${target}" is not a document target`);
|
|
1316
|
+
const files = buildDoc({ config, root, target });
|
|
1317
|
+
const many = files.length > 1 || target === "cursor";
|
|
1318
|
+
const generated = files.map((file) => many ? `${MARKER(file.name)}
|
|
1319
|
+
${file.contents}` : file.contents).join("\n");
|
|
1320
|
+
const at = (where, file) => many ? resolve11(root, where, file.name) : resolve11(root, where);
|
|
1321
|
+
if (write !== void 0) {
|
|
1322
|
+
for (const file of files) {
|
|
1323
|
+
const path = at(write, file);
|
|
1324
|
+
mkdirSync4(dirname2(path), { recursive: true });
|
|
1325
|
+
writeFileSync5(path, file.contents);
|
|
1326
|
+
}
|
|
1327
|
+
return { generated, ok: true, written: write };
|
|
1328
|
+
}
|
|
1329
|
+
if (check !== void 0) {
|
|
1330
|
+
const differing = files.flatMap((file) => {
|
|
1331
|
+
const path = at(check, file);
|
|
1332
|
+
const committed = existsSync11(path) ? readFileSync10(path, "utf8") : void 0;
|
|
1333
|
+
return committed === file.contents ? [] : [
|
|
1334
|
+
[
|
|
1335
|
+
`${relative2(root, path).replaceAll("\\", "/")} \u2014 ${committed === void 0 ? "is not there" : "differs"}`,
|
|
1336
|
+
"committed:",
|
|
1337
|
+
committed ?? "(nothing)",
|
|
1338
|
+
"generated:",
|
|
1339
|
+
file.contents
|
|
1340
|
+
].join("\n")
|
|
1341
|
+
];
|
|
1342
|
+
});
|
|
1343
|
+
return {
|
|
1344
|
+
...differing.length === 0 ? {} : { diff: differing.join("\n\n") },
|
|
1345
|
+
generated,
|
|
1346
|
+
ok: differing.length === 0
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
return { generated, ok: true };
|
|
1350
|
+
};
|
|
1351
|
+
var runSync = ({
|
|
1352
|
+
check,
|
|
1353
|
+
config,
|
|
1354
|
+
packageName,
|
|
1355
|
+
root,
|
|
1356
|
+
target,
|
|
1357
|
+
write
|
|
1358
|
+
}) => {
|
|
1359
|
+
if (!SYNC_TARGETS.includes(target)) {
|
|
1360
|
+
throw new Error(`"${target}" is not a target \u2014 one of ${SYNC_TARGETS.join(", ")}`);
|
|
1361
|
+
}
|
|
1362
|
+
if (isDocTarget(target)) {
|
|
1363
|
+
return runDocSync({
|
|
1364
|
+
...check === void 0 ? {} : { check },
|
|
1365
|
+
config,
|
|
1366
|
+
root,
|
|
1367
|
+
target,
|
|
1368
|
+
...write === void 0 ? {} : { write }
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
const report = checkArchitecture({ config, root, workspace: target !== "turbo" });
|
|
1372
|
+
if (report.violations.length > 0) {
|
|
1373
|
+
throw new Error(`${config.architecture} does not check out:
|
|
1374
|
+
${report.violations.join("\n")}`);
|
|
1375
|
+
}
|
|
1376
|
+
const packages = report.packages ?? [];
|
|
1377
|
+
const path = target === "turbo" ? TURBO_PATH : target === "layer-walls" ? LAYERS_PATH : RESTRICTED_PATH;
|
|
1378
|
+
const wholeValue = target === "turbo" ? turboOf(report.edges) : target === "layer-walls" ? layersOf(report.edges, packages) : restrictedOf(report.edges, packages);
|
|
1379
|
+
const forFile = (file) => {
|
|
1380
|
+
if (target !== "restricted-imports") return wholeValue;
|
|
1381
|
+
const owner = packageName ?? packageAt(file, root, packages);
|
|
1382
|
+
if (owner === void 0) throw new Error(`${file} is not inside a workspace package`);
|
|
1383
|
+
const banned = wholeValue[owner];
|
|
1384
|
+
if (banned === void 0) throw new Error(`${owner} carries no tag in the edges table`);
|
|
1385
|
+
return banned;
|
|
1386
|
+
};
|
|
1387
|
+
const generated = JSON.stringify(wholeValue, void 0, 2);
|
|
1388
|
+
if (write !== void 0) {
|
|
1389
|
+
const at = resolve11(root, write);
|
|
1390
|
+
if (!existsSync11(at)) throw new Error(`${write} does not exist`);
|
|
1391
|
+
const source = readFileSync10(at, "utf8");
|
|
1392
|
+
writeFileSync5(at, upsertJsonValue(source, path, forFile(write), grow));
|
|
1393
|
+
return { generated, ok: true, written: write };
|
|
1394
|
+
}
|
|
1395
|
+
if (check !== void 0) {
|
|
1396
|
+
const at = resolve11(root, check);
|
|
1397
|
+
if (!existsSync11(at)) throw new Error(`${check} does not exist`);
|
|
1398
|
+
const committed = readJsonValue(readFileSync10(at, "utf8"), path);
|
|
1399
|
+
const wanted = forFile(check);
|
|
1400
|
+
const ok = JSON.stringify(committed) === JSON.stringify(wanted);
|
|
1401
|
+
return {
|
|
1402
|
+
...ok ? {} : {
|
|
1403
|
+
diff: [
|
|
1404
|
+
`${check} \u2014 ${path.join(".")}`,
|
|
1405
|
+
"committed:",
|
|
1406
|
+
JSON.stringify(committed, void 0, 2),
|
|
1407
|
+
"generated:",
|
|
1408
|
+
JSON.stringify(wanted, void 0, 2)
|
|
1409
|
+
].join("\n")
|
|
1410
|
+
},
|
|
1411
|
+
generated,
|
|
1412
|
+
ok
|
|
1413
|
+
};
|
|
1414
|
+
}
|
|
1415
|
+
return { generated, ok: true };
|
|
1416
|
+
};
|
|
1417
|
+
|
|
1418
|
+
// src/prove.ts
|
|
1419
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
1420
|
+
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync5, mkdtempSync, rmSync, writeFileSync as writeFileSync6 } from "fs";
|
|
1421
|
+
import { tmpdir } from "os";
|
|
1422
|
+
import { dirname as dirname3, join as join5 } from "path";
|
|
1423
|
+
var run = (args, cwd) => {
|
|
1424
|
+
const done = spawnSync3("git", args, {
|
|
1425
|
+
cwd,
|
|
1426
|
+
encoding: "utf8",
|
|
1427
|
+
env: { ...process.env, GIT_CONFIG_GLOBAL: "/dev/null", GIT_CONFIG_SYSTEM: "/dev/null" }
|
|
1428
|
+
});
|
|
1429
|
+
if (done.status !== 0) throw new Error(`git ${args.join(" ")}: ${done.stderr}`);
|
|
1430
|
+
};
|
|
1431
|
+
var put = (root, file, body) => {
|
|
1432
|
+
mkdirSync5(dirname3(join5(root, file)), { recursive: true });
|
|
1433
|
+
writeFileSync6(join5(root, file), body);
|
|
1434
|
+
};
|
|
1435
|
+
var scratch = () => {
|
|
1436
|
+
const root = mkdtempSync(join5(tmpdir(), "geonosis-ledger-prove-"));
|
|
1437
|
+
run(["init", "--initial-branch=main"], root);
|
|
1438
|
+
appendFileSync2(
|
|
1439
|
+
join5(root, ".git/config"),
|
|
1440
|
+
"[user]\n email = ledger@example.test\n name = Ledger Prove\n[commit]\n gpgsign = false\n"
|
|
1441
|
+
);
|
|
1442
|
+
return root;
|
|
1443
|
+
};
|
|
1444
|
+
var runProve = (config) => {
|
|
1445
|
+
const probes = [];
|
|
1446
|
+
const long = [
|
|
1447
|
+
"feat(x): a thing",
|
|
1448
|
+
"",
|
|
1449
|
+
...Array.from({ length: 39 }, (_, at) => `narration line ${at + 1}.`)
|
|
1450
|
+
].join("\n");
|
|
1451
|
+
probes.push({
|
|
1452
|
+
finding: "a 40-line commit message",
|
|
1453
|
+
gate: "commit-msg",
|
|
1454
|
+
...checkCommitMessage(long, config)
|
|
1455
|
+
});
|
|
1456
|
+
const root = scratch();
|
|
1457
|
+
try {
|
|
1458
|
+
put(root, "src/a.ts", "export const a = 1\n");
|
|
1459
|
+
run(["add", "-A"], root);
|
|
1460
|
+
run(["commit", "-m", "feat: base"], root);
|
|
1461
|
+
put(root, "src/a.ts", "// what a is\n/* and why */\nexport const a = 1\n");
|
|
1462
|
+
run(["add", "-A"], root);
|
|
1463
|
+
probes.push({
|
|
1464
|
+
finding: "a diff whose every changed line is a comment",
|
|
1465
|
+
gate: "delivery check",
|
|
1466
|
+
...checkDelivery({ root, staged: true })
|
|
1467
|
+
});
|
|
1468
|
+
} finally {
|
|
1469
|
+
rmSync(root, { force: true, recursive: true });
|
|
1470
|
+
}
|
|
1471
|
+
return { ok: probes.every((probe) => probe.refusal !== void 0), probes };
|
|
1472
|
+
};
|
|
1473
|
+
var formatProve = (report) => [
|
|
1474
|
+
...report.probes.map(
|
|
1475
|
+
(probe) => probe.refusal === void 0 ? ` CANNOT FAIL ${probe.gate}: took ${probe.finding}` : ` PROVEN ${probe.gate}: refuses ${probe.finding}`
|
|
1476
|
+
),
|
|
1477
|
+
"",
|
|
1478
|
+
report.ok ? "prove PASS \u2014 every delivery gate refused the thing it forbids." : "prove FAIL \u2014 a gate took what it forbids. It is not a gate.",
|
|
1479
|
+
""
|
|
1480
|
+
].join("\n");
|
|
1481
|
+
|
|
1482
|
+
export {
|
|
1483
|
+
parseArgs,
|
|
1484
|
+
required,
|
|
1485
|
+
rejectUnknownSwitches,
|
|
1486
|
+
LEDGER_DEFAULT,
|
|
1487
|
+
loadLedgerConfig,
|
|
1488
|
+
matchesRuntime,
|
|
1489
|
+
headingsOf,
|
|
1490
|
+
sectionUnder,
|
|
1491
|
+
firstTable,
|
|
1492
|
+
rowCells,
|
|
1493
|
+
plainCell,
|
|
1494
|
+
renderRow,
|
|
1495
|
+
checkPlans,
|
|
1496
|
+
planStatuses,
|
|
1497
|
+
nextNumber,
|
|
1498
|
+
newPlan,
|
|
1499
|
+
isoDate,
|
|
1500
|
+
appendRow,
|
|
1501
|
+
FALLBACK_KINDS,
|
|
1502
|
+
JOURNAL_HEADER,
|
|
1503
|
+
appendFallback,
|
|
1504
|
+
messageOf,
|
|
1505
|
+
captureProof,
|
|
1506
|
+
countedLines,
|
|
1507
|
+
checkCommitMessage,
|
|
1508
|
+
checkDelivery,
|
|
1509
|
+
PROGRESS_HEADER,
|
|
1510
|
+
runTick,
|
|
1511
|
+
REVISIT_COLUMN,
|
|
1512
|
+
runDecide,
|
|
1513
|
+
GATE_REPORT,
|
|
1514
|
+
gateSummary,
|
|
1515
|
+
baselineKeyCount,
|
|
1516
|
+
CLOSE_START,
|
|
1517
|
+
CLOSE_END,
|
|
1518
|
+
runHandoff,
|
|
1519
|
+
STATUS_MAX_LINES,
|
|
1520
|
+
readStatus,
|
|
1521
|
+
formatStatus,
|
|
1522
|
+
EDGES_HEADER,
|
|
1523
|
+
readEdges,
|
|
1524
|
+
workspacePackages,
|
|
1525
|
+
checkArchitecture,
|
|
1526
|
+
replaceJsonValue,
|
|
1527
|
+
readJsonValue,
|
|
1528
|
+
DOC_TARGETS,
|
|
1529
|
+
isDocTarget,
|
|
1530
|
+
buildAgentsMd,
|
|
1531
|
+
buildCursorRules,
|
|
1532
|
+
buildDoc,
|
|
1533
|
+
SYNC_TARGETS,
|
|
1534
|
+
LAYER_WALLS_RULE,
|
|
1535
|
+
runSync,
|
|
1536
|
+
runProve,
|
|
1537
|
+
formatProve
|
|
1538
|
+
};
|