codesteward 0.0.1
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 +201 -0
- package/README.md +107 -0
- package/dist/main.js +2711 -0
- package/dist/main.js.map +6 -0
- package/dist/templates/instructions/agent-instructions.md +45 -0
- package/dist/templates/skills/claude/decision-aware-design/SKILL.md +21 -0
- package/dist/templates/skills/claude/decision-aware-implement/SKILL.md +21 -0
- package/dist/templates/skills/codex/decision-aware-design/SKILL.md +20 -0
- package/dist/templates/skills/codex/decision-aware-implement/SKILL.md +20 -0
- package/dist/templates/skills/generic/decision-aware-design/SKILL.md +21 -0
- package/dist/templates/skills/generic/decision-aware-implement/SKILL.md +21 -0
- package/package.json +46 -0
package/dist/main.js
ADDED
|
@@ -0,0 +1,2711 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
13
|
+
var __copyProps = (to, from, except, desc) => {
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
17
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
22
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
23
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
24
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
25
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
26
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
27
|
+
mod
|
|
28
|
+
));
|
|
29
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
30
|
+
|
|
31
|
+
// src/main.ts
|
|
32
|
+
var main_exports = {};
|
|
33
|
+
__export(main_exports, {
|
|
34
|
+
bootstrapCommand: () => bootstrapCommand,
|
|
35
|
+
main: () => main,
|
|
36
|
+
runBootstrapCommand: () => runBootstrapCommand
|
|
37
|
+
});
|
|
38
|
+
module.exports = __toCommonJS(main_exports);
|
|
39
|
+
var import_node_child_process = require("node:child_process");
|
|
40
|
+
var fs6 = __toESM(require("node:fs/promises"));
|
|
41
|
+
var path5 = __toESM(require("node:path"));
|
|
42
|
+
|
|
43
|
+
// src/core/candidates.ts
|
|
44
|
+
var fs4 = __toESM(require("node:fs/promises"));
|
|
45
|
+
var path3 = __toESM(require("node:path"));
|
|
46
|
+
|
|
47
|
+
// src/core/dr.ts
|
|
48
|
+
var fs2 = __toESM(require("node:fs/promises"));
|
|
49
|
+
var path2 = __toESM(require("node:path"));
|
|
50
|
+
|
|
51
|
+
// src/core/store.ts
|
|
52
|
+
var fs = __toESM(require("node:fs/promises"));
|
|
53
|
+
var path = __toESM(require("node:path"));
|
|
54
|
+
|
|
55
|
+
// src/core/harnesses.ts
|
|
56
|
+
function selectedHarnessInstallers(options) {
|
|
57
|
+
const installers = [];
|
|
58
|
+
if (options.claude === true) {
|
|
59
|
+
installers.push(claudeInstaller);
|
|
60
|
+
}
|
|
61
|
+
if (options.codex === true) {
|
|
62
|
+
installers.push(codexInstaller);
|
|
63
|
+
}
|
|
64
|
+
return installers;
|
|
65
|
+
}
|
|
66
|
+
var claudeInstaller = {
|
|
67
|
+
name: "claude",
|
|
68
|
+
root: ".claude",
|
|
69
|
+
async install(context) {
|
|
70
|
+
await context.ensureManagedInstructions({
|
|
71
|
+
relativePath: ".claude/CLAUDE.md",
|
|
72
|
+
title: "Claude Code Instructions"
|
|
73
|
+
});
|
|
74
|
+
await context.ensureSkillTemplates({
|
|
75
|
+
root: ".claude",
|
|
76
|
+
preferredTemplateSet: "claude"
|
|
77
|
+
});
|
|
78
|
+
},
|
|
79
|
+
async update(context) {
|
|
80
|
+
await context.ensureManagedInstructions({
|
|
81
|
+
relativePath: ".claude/CLAUDE.md",
|
|
82
|
+
title: "Claude Code Instructions"
|
|
83
|
+
});
|
|
84
|
+
await context.updateSkillTemplates({
|
|
85
|
+
root: ".claude",
|
|
86
|
+
preferredTemplateSet: "claude"
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
var codexInstaller = {
|
|
91
|
+
name: "codex",
|
|
92
|
+
root: ".agents",
|
|
93
|
+
async install(context) {
|
|
94
|
+
await context.ensureManagedInstructions({
|
|
95
|
+
relativePath: "AGENTS.md",
|
|
96
|
+
title: "Agent Instructions"
|
|
97
|
+
});
|
|
98
|
+
await context.ensureSkillTemplates({
|
|
99
|
+
root: ".agents",
|
|
100
|
+
preferredTemplateSet: "codex"
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
async update(context) {
|
|
104
|
+
await context.ensureManagedInstructions({
|
|
105
|
+
relativePath: "AGENTS.md",
|
|
106
|
+
title: "Agent Instructions"
|
|
107
|
+
});
|
|
108
|
+
await context.updateSkillTemplates({
|
|
109
|
+
root: ".agents",
|
|
110
|
+
preferredTemplateSet: "codex"
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// src/core/store.ts
|
|
116
|
+
var storeDirectoryName = ".codesteward";
|
|
117
|
+
var codestewardInstructionStartMarker = "<!-- codesteward:agent-instructions -->";
|
|
118
|
+
var codestewardInstructionEndMarker = "<!-- /codesteward:agent-instructions -->";
|
|
119
|
+
var legacyCodestewardInstructionStartMarker = "<!-- codesteward:correction-feedback-loop -->";
|
|
120
|
+
var legacyCodestewardInstructionEndMarker = "<!-- /codesteward:correction-feedback-loop -->";
|
|
121
|
+
var directoryLayout = [
|
|
122
|
+
"drs/candidates",
|
|
123
|
+
"drs/accepted",
|
|
124
|
+
"drs/rejected",
|
|
125
|
+
"drs/retired",
|
|
126
|
+
"sessions"
|
|
127
|
+
];
|
|
128
|
+
var skillTemplateFiles = [
|
|
129
|
+
"decision-aware-design/SKILL.md",
|
|
130
|
+
"decision-aware-implement/SKILL.md"
|
|
131
|
+
];
|
|
132
|
+
var decisionRecordStatuses = ["candidate", "accepted", "rejected", "retired"];
|
|
133
|
+
function getStorePaths(projectRoot) {
|
|
134
|
+
const root = path.resolve(projectRoot);
|
|
135
|
+
const store = path.join(root, storeDirectoryName);
|
|
136
|
+
return {
|
|
137
|
+
root,
|
|
138
|
+
store,
|
|
139
|
+
config: path.join(store, "config.json"),
|
|
140
|
+
tags: path.join(store, "tags.md")
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
async function discoverStore(startDirectory) {
|
|
144
|
+
let current = path.resolve(startDirectory);
|
|
145
|
+
for (; ; ) {
|
|
146
|
+
const paths = getStorePaths(current);
|
|
147
|
+
if (await pathExists(paths.store)) {
|
|
148
|
+
return paths;
|
|
149
|
+
}
|
|
150
|
+
const parent = path.dirname(current);
|
|
151
|
+
if (parent === current) {
|
|
152
|
+
return void 0;
|
|
153
|
+
}
|
|
154
|
+
current = parent;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async function initStore(projectRoot, options = {}) {
|
|
158
|
+
const paths = getStorePaths(projectRoot);
|
|
159
|
+
const created = [];
|
|
160
|
+
const existing = [];
|
|
161
|
+
const updated = [];
|
|
162
|
+
await ensureDirectory(paths.store, created, existing, paths.root);
|
|
163
|
+
for (const relativeDirectory of directoryLayout) {
|
|
164
|
+
await ensureDirectory(path.join(paths.store, relativeDirectory), created, existing, paths.root);
|
|
165
|
+
}
|
|
166
|
+
await ensureFile(paths.config, defaultConfig(), created, existing, paths.root);
|
|
167
|
+
await ensureFile(paths.tags, defaultTags(), created, existing, paths.root);
|
|
168
|
+
const harnessInstallers = selectedHarnessInstallers(options);
|
|
169
|
+
const context = createHarnessInstallContext(paths, harnessInstallers, created, existing, updated);
|
|
170
|
+
for (const installer of harnessInstallers) {
|
|
171
|
+
await installer.install(context);
|
|
172
|
+
}
|
|
173
|
+
return { paths, created, existing, updated };
|
|
174
|
+
}
|
|
175
|
+
async function updateRuntimeAssets(projectRoot, options) {
|
|
176
|
+
const paths = getStorePaths(projectRoot);
|
|
177
|
+
const created = [];
|
|
178
|
+
const existing = [];
|
|
179
|
+
const updated = [];
|
|
180
|
+
const harnessInstallers = selectedHarnessInstallers(options);
|
|
181
|
+
const context = createHarnessInstallContext(paths, harnessInstallers, created, existing, updated);
|
|
182
|
+
for (const installer of harnessInstallers) {
|
|
183
|
+
await installer.update(context);
|
|
184
|
+
}
|
|
185
|
+
return { paths, created, existing, updated };
|
|
186
|
+
}
|
|
187
|
+
async function pathExists(filePath) {
|
|
188
|
+
try {
|
|
189
|
+
await fs.access(filePath);
|
|
190
|
+
return true;
|
|
191
|
+
} catch (error) {
|
|
192
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
async function countDecisionRecords(paths, status) {
|
|
199
|
+
const directory = decisionRecordDirectory(paths, status);
|
|
200
|
+
if (!await pathExists(directory)) {
|
|
201
|
+
return 0;
|
|
202
|
+
}
|
|
203
|
+
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
204
|
+
return entries.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith(".md")).length;
|
|
205
|
+
}
|
|
206
|
+
function decisionRecordDirectory(paths, status) {
|
|
207
|
+
const directoryName = status === "candidate" ? "candidates" : status;
|
|
208
|
+
return path.join(paths.store, "drs", directoryName);
|
|
209
|
+
}
|
|
210
|
+
async function ensureDirectory(directoryPath, created, existing, projectRoot) {
|
|
211
|
+
if (await pathExists(directoryPath)) {
|
|
212
|
+
existing.push(formatRelative(projectRoot, directoryPath));
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
await fs.mkdir(directoryPath, { recursive: true });
|
|
216
|
+
created.push(formatRelative(projectRoot, directoryPath));
|
|
217
|
+
}
|
|
218
|
+
async function ensureFile(filePath, contents, created, existing, projectRoot) {
|
|
219
|
+
if (await pathExists(filePath)) {
|
|
220
|
+
existing.push(formatRelative(projectRoot, filePath));
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
224
|
+
await fs.writeFile(filePath, contents, "utf8");
|
|
225
|
+
created.push(formatRelative(projectRoot, filePath));
|
|
226
|
+
}
|
|
227
|
+
function defaultConfig() {
|
|
228
|
+
return `${JSON.stringify({
|
|
229
|
+
version: 1,
|
|
230
|
+
store: storeDirectoryName
|
|
231
|
+
}, void 0, 2)}
|
|
232
|
+
`;
|
|
233
|
+
}
|
|
234
|
+
function defaultTags() {
|
|
235
|
+
return [
|
|
236
|
+
"# CodeSteward Vocabulary",
|
|
237
|
+
"",
|
|
238
|
+
"Domains are broad applicability scopes. Use lowercase dot-separated hierarchy paths. A domain query matches ancestors, the exact domain, and descendants, but not sibling branches.",
|
|
239
|
+
"",
|
|
240
|
+
"Tags are concern filters used within matching domains. Apply every applicable tag when it helps retrieval; omit tags only when a DR should match every tag query in its domain.",
|
|
241
|
+
"",
|
|
242
|
+
"## Domains",
|
|
243
|
+
"",
|
|
244
|
+
"### all",
|
|
245
|
+
"",
|
|
246
|
+
"Global guidance that applies across the project.",
|
|
247
|
+
"",
|
|
248
|
+
"### cli",
|
|
249
|
+
"",
|
|
250
|
+
"Command-line behavior and CLI-owned workflows.",
|
|
251
|
+
"",
|
|
252
|
+
"## Tags",
|
|
253
|
+
"",
|
|
254
|
+
"### architecture",
|
|
255
|
+
"",
|
|
256
|
+
"Project-level architecture choices and system boundaries.",
|
|
257
|
+
""
|
|
258
|
+
].join("\n");
|
|
259
|
+
}
|
|
260
|
+
function createHarnessInstallContext(paths, harnessInstallers, created, existing, updated) {
|
|
261
|
+
const processedInstructionTargets = /* @__PURE__ */ new Set();
|
|
262
|
+
let sharedSkillTargets;
|
|
263
|
+
async function selectedSkillTemplateSet(input) {
|
|
264
|
+
sharedSkillTargets ??= selectedHarnessesShareSkillTargets(paths.root, harnessInstallers);
|
|
265
|
+
return await sharedSkillTargets ? "generic" : input.preferredTemplateSet;
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
async ensureManagedInstructions(target) {
|
|
269
|
+
const rule = await renderTemplate("instructions/agent-instructions.md");
|
|
270
|
+
await ensureManagedInstructionBlock(
|
|
271
|
+
path.join(paths.root, target.relativePath),
|
|
272
|
+
target.title,
|
|
273
|
+
rule,
|
|
274
|
+
created,
|
|
275
|
+
existing,
|
|
276
|
+
updated,
|
|
277
|
+
paths.root,
|
|
278
|
+
processedInstructionTargets
|
|
279
|
+
);
|
|
280
|
+
},
|
|
281
|
+
async ensureSkillTemplates(input) {
|
|
282
|
+
const templateSet = await selectedSkillTemplateSet(input);
|
|
283
|
+
for (const asset of skillTemplateAssets(input.root, templateSet)) {
|
|
284
|
+
await ensureFile(path.join(paths.root, asset.relativePath), await renderTemplate(asset.templatePath), created, existing, paths.root);
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
async updateSkillTemplates(input) {
|
|
288
|
+
const templateSet = await selectedSkillTemplateSet(input);
|
|
289
|
+
for (const asset of skillTemplateAssets(input.root, templateSet)) {
|
|
290
|
+
await updateFile(path.join(paths.root, asset.relativePath), await renderTemplate(asset.templatePath), created, existing, updated, paths.root);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
async function ensureManagedInstructionBlock(target, title, rule, created, existing, updated, projectRoot, processedTargets) {
|
|
296
|
+
if (!await pathExists(target)) {
|
|
297
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
298
|
+
await fs.writeFile(target, `# ${title}
|
|
299
|
+
|
|
300
|
+
${rule}`, "utf8");
|
|
301
|
+
processedTargets.add(await physicalPathIdentity(target));
|
|
302
|
+
created.push(formatRelative(projectRoot, target));
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const identity = await physicalPathIdentity(target);
|
|
306
|
+
if (processedTargets.has(identity)) {
|
|
307
|
+
const current2 = await fs.readFile(target, "utf8");
|
|
308
|
+
const repaired2 = repairManagedInstructionBlock(current2, rule);
|
|
309
|
+
if (repaired2 === current2) {
|
|
310
|
+
existing.push(formatRelative(projectRoot, target));
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
await fs.writeFile(target, repaired2, "utf8");
|
|
314
|
+
updated.push(formatRelative(projectRoot, target));
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const current = await fs.readFile(target, "utf8");
|
|
318
|
+
const repaired = repairManagedInstructionBlock(current, rule);
|
|
319
|
+
if (repaired === current) {
|
|
320
|
+
processedTargets.add(identity);
|
|
321
|
+
existing.push(formatRelative(projectRoot, target));
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
await fs.writeFile(target, repaired, "utf8");
|
|
325
|
+
processedTargets.add(await physicalPathIdentity(target));
|
|
326
|
+
updated.push(formatRelative(projectRoot, target));
|
|
327
|
+
}
|
|
328
|
+
function repairManagedInstructionBlock(current, rule) {
|
|
329
|
+
const marker = findManagedInstructionMarker(current);
|
|
330
|
+
const start = marker.start;
|
|
331
|
+
if (start === -1) {
|
|
332
|
+
const separator = current.endsWith("\n") ? "\n" : "\n\n";
|
|
333
|
+
return `${current}${separator}${rule}`;
|
|
334
|
+
}
|
|
335
|
+
const end = current.indexOf(marker.endMarker, start);
|
|
336
|
+
if (end === -1) {
|
|
337
|
+
return `${current.slice(0, start)}${rule}`;
|
|
338
|
+
}
|
|
339
|
+
const endAfterMarker = end + marker.endMarker.length;
|
|
340
|
+
const afterEnd = current.slice(endAfterMarker).startsWith("\n") ? current.slice(endAfterMarker + 1) : current.slice(endAfterMarker);
|
|
341
|
+
return `${current.slice(0, start)}${rule}${afterEnd}`;
|
|
342
|
+
}
|
|
343
|
+
function findManagedInstructionMarker(current) {
|
|
344
|
+
const start = current.indexOf(codestewardInstructionStartMarker);
|
|
345
|
+
if (start !== -1) {
|
|
346
|
+
return { start, endMarker: codestewardInstructionEndMarker };
|
|
347
|
+
}
|
|
348
|
+
return {
|
|
349
|
+
start: current.indexOf(legacyCodestewardInstructionStartMarker),
|
|
350
|
+
endMarker: legacyCodestewardInstructionEndMarker
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
async function selectedHarnessesShareSkillTargets(projectRoot, harnessInstallers) {
|
|
354
|
+
if (harnessInstallers.length < 2) {
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
for (const skillFile of skillTemplateFiles) {
|
|
358
|
+
const identities = /* @__PURE__ */ new Map();
|
|
359
|
+
for (const installer of harnessInstallers) {
|
|
360
|
+
const skillPath = path.join(projectRoot, installer.root, "skills", ...skillFile.split("/"));
|
|
361
|
+
const identity = await physicalPathIdentity(skillPath);
|
|
362
|
+
const existingRoot = identities.get(identity);
|
|
363
|
+
if (existingRoot !== void 0 && existingRoot !== installer.root) {
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
366
|
+
identities.set(identity, installer.root);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
async function physicalPathIdentity(filePath) {
|
|
372
|
+
const missingParts = [];
|
|
373
|
+
let current = path.resolve(filePath);
|
|
374
|
+
for (; ; ) {
|
|
375
|
+
try {
|
|
376
|
+
return path.join(await fs.realpath(current), ...missingParts.reverse());
|
|
377
|
+
} catch (error) {
|
|
378
|
+
if (!isNodeError(error) || error.code !== "ENOENT") {
|
|
379
|
+
throw error;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
const parent = path.dirname(current);
|
|
383
|
+
if (parent === current) {
|
|
384
|
+
return path.resolve(filePath);
|
|
385
|
+
}
|
|
386
|
+
missingParts.push(path.basename(current));
|
|
387
|
+
current = parent;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
async function updateFile(filePath, contents, created, existing, updated, projectRoot) {
|
|
391
|
+
if (!await pathExists(filePath)) {
|
|
392
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
393
|
+
await fs.writeFile(filePath, contents, "utf8");
|
|
394
|
+
created.push(formatRelative(projectRoot, filePath));
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
const current = await fs.readFile(filePath, "utf8");
|
|
398
|
+
if (current === contents) {
|
|
399
|
+
existing.push(formatRelative(projectRoot, filePath));
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
await fs.writeFile(filePath, contents, "utf8");
|
|
403
|
+
updated.push(formatRelative(projectRoot, filePath));
|
|
404
|
+
}
|
|
405
|
+
function skillTemplateAssets(root, templateSet) {
|
|
406
|
+
return skillTemplateFiles.map((skillFile) => ({
|
|
407
|
+
relativePath: `${root}/skills/${skillFile}`,
|
|
408
|
+
templatePath: `skills/${templateSet}/${skillFile}`
|
|
409
|
+
}));
|
|
410
|
+
}
|
|
411
|
+
var templateIncludes = {
|
|
412
|
+
crHowTo: "include/crHowTo.md"
|
|
413
|
+
};
|
|
414
|
+
async function renderTemplate(relativePath, seen = []) {
|
|
415
|
+
if (seen.includes(relativePath)) {
|
|
416
|
+
throw new Error(`Circular CodeSteward template include: ${[...seen, relativePath].join(" -> ")}`);
|
|
417
|
+
}
|
|
418
|
+
let contents = await readTemplate(relativePath);
|
|
419
|
+
for (const [name, includePath] of Object.entries(templateIncludes)) {
|
|
420
|
+
const token = `{{${name}}}`;
|
|
421
|
+
if (contents.includes(token)) {
|
|
422
|
+
const include = (await renderTemplate(includePath, [...seen, relativePath])).trimEnd();
|
|
423
|
+
contents = contents.replaceAll(token, include);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return ensureTrailingNewline(contents);
|
|
427
|
+
}
|
|
428
|
+
async function readTemplate(relativePath) {
|
|
429
|
+
const errors = [];
|
|
430
|
+
for (const candidate of templatePathCandidates(relativePath)) {
|
|
431
|
+
try {
|
|
432
|
+
return await fs.readFile(candidate, "utf8");
|
|
433
|
+
} catch (error) {
|
|
434
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
435
|
+
errors.push(candidate);
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
throw error;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
throw new Error(`CodeSteward template not found: ${relativePath} (searched ${errors.join(", ")})`);
|
|
442
|
+
}
|
|
443
|
+
function templatePathCandidates(relativePath) {
|
|
444
|
+
const parts = relativePath.split("/");
|
|
445
|
+
return [
|
|
446
|
+
path.join(__dirname, "templates", ...parts),
|
|
447
|
+
path.join(__dirname, "..", "src", "core", "templates", ...parts),
|
|
448
|
+
path.join(__dirname, "..", "..", "src", "core", "templates", ...parts)
|
|
449
|
+
];
|
|
450
|
+
}
|
|
451
|
+
function ensureTrailingNewline(contents) {
|
|
452
|
+
return contents.endsWith("\n") ? contents : `${contents}
|
|
453
|
+
`;
|
|
454
|
+
}
|
|
455
|
+
function formatRelative(projectRoot, targetPath) {
|
|
456
|
+
const relativePath = path.relative(projectRoot, targetPath);
|
|
457
|
+
return relativePath.length === 0 ? "." : relativePath.split(path.sep).join("/");
|
|
458
|
+
}
|
|
459
|
+
function isNodeError(error) {
|
|
460
|
+
return error instanceof Error && "code" in error;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// src/core/dr.ts
|
|
464
|
+
var statusValues = new Set(decisionRecordStatuses);
|
|
465
|
+
var drIdPattern = /^DR-\d{4}$/;
|
|
466
|
+
var candidateIdPattern = /^CAND-\d{4}$/;
|
|
467
|
+
var isoDatePattern = /^\d{4}-\d{2}-\d{2}$/;
|
|
468
|
+
var domainPattern = /^(?:all|[a-z0-9]+(?:-[a-z0-9]+)*(?:\.[a-z0-9]+(?:-[a-z0-9]+)*)*)$/;
|
|
469
|
+
async function readDecisionRecord(filePath, expectedStatus) {
|
|
470
|
+
const markdown = await fs2.readFile(filePath, "utf8");
|
|
471
|
+
return parseDecisionRecord(markdown, filePath, expectedStatus);
|
|
472
|
+
}
|
|
473
|
+
function parseDecisionRecord(markdown, filePath, expectedStatus) {
|
|
474
|
+
const frontmatterMatch = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)([\s\S]*)$/.exec(markdown);
|
|
475
|
+
if (frontmatterMatch === null) {
|
|
476
|
+
return {
|
|
477
|
+
filePath,
|
|
478
|
+
expectedStatus,
|
|
479
|
+
markdown,
|
|
480
|
+
frontmatter: {},
|
|
481
|
+
body: markdown,
|
|
482
|
+
sections: parseSections(markdown)
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
const [, frontmatterText, body] = frontmatterMatch;
|
|
486
|
+
return {
|
|
487
|
+
filePath,
|
|
488
|
+
expectedStatus,
|
|
489
|
+
markdown,
|
|
490
|
+
frontmatter: parseFrontmatter(frontmatterText),
|
|
491
|
+
body,
|
|
492
|
+
sections: parseSections(body)
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
async function listDecisionRecords(paths, status) {
|
|
496
|
+
const statuses = status === void 0 ? decisionRecordStatuses : [status];
|
|
497
|
+
const records = [];
|
|
498
|
+
for (const item of statuses) {
|
|
499
|
+
const directory = decisionRecordDirectory(paths, item);
|
|
500
|
+
const entries = await safeReadDirectory(directory);
|
|
501
|
+
for (const entry of entries) {
|
|
502
|
+
if (!entry.isFile() || !entry.name.toLowerCase().endsWith(".md")) {
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
505
|
+
records.push(await readDecisionRecord(path2.join(directory, entry.name), item));
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return records.sort((left, right) => getRequiredString(left, "id").localeCompare(getRequiredString(right, "id")));
|
|
509
|
+
}
|
|
510
|
+
function validateDecisionRecord(record, vocabulary) {
|
|
511
|
+
const errors = [];
|
|
512
|
+
const warnings = [];
|
|
513
|
+
const frontmatter = record.frontmatter;
|
|
514
|
+
const status = getRequiredString(record, "status");
|
|
515
|
+
for (const field of ["id", "title", "status", "created"]) {
|
|
516
|
+
if (!hasFrontmatterValue(frontmatter, field)) {
|
|
517
|
+
errors.push(`Missing required field "${field}".`);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (status.length > 0 && !statusValues.has(status)) {
|
|
521
|
+
errors.push(`Invalid status "${status}".`);
|
|
522
|
+
}
|
|
523
|
+
if (record.expectedStatus !== void 0 && status.length > 0 && record.expectedStatus !== status) {
|
|
524
|
+
errors.push(`Status "${status}" does not match ${record.expectedStatus} folder.`);
|
|
525
|
+
}
|
|
526
|
+
validateId(record, errors);
|
|
527
|
+
validateDate(record, "created", errors);
|
|
528
|
+
validateDate(record, "updated", errors);
|
|
529
|
+
validateBoolean(record, "enabled", errors);
|
|
530
|
+
validateDomain(record, vocabulary, errors, warnings);
|
|
531
|
+
validateTags(record, vocabulary, errors, warnings);
|
|
532
|
+
validateRemovedRetrievalFields(record, errors);
|
|
533
|
+
if (status === "accepted") {
|
|
534
|
+
for (const field of ["updated", "author"]) {
|
|
535
|
+
if (!hasFrontmatterValue(frontmatter, field)) {
|
|
536
|
+
errors.push(`Missing required accepted-DR field "${field}".`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
if (!record.sections.has("decision")) {
|
|
540
|
+
errors.push('Missing required "## Decision" section.');
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return {
|
|
544
|
+
path: record.filePath,
|
|
545
|
+
errors,
|
|
546
|
+
warnings
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
function getRecordTags(record) {
|
|
550
|
+
return getStringList(record, "tags");
|
|
551
|
+
}
|
|
552
|
+
function getRecordId(record) {
|
|
553
|
+
return getRequiredString(record, "id");
|
|
554
|
+
}
|
|
555
|
+
function getRecordTitle(record) {
|
|
556
|
+
return getRequiredString(record, "title");
|
|
557
|
+
}
|
|
558
|
+
function getRecordStatus(record) {
|
|
559
|
+
return getRequiredString(record, "status");
|
|
560
|
+
}
|
|
561
|
+
function getRecordEnabled(record) {
|
|
562
|
+
return getOptionalString(record, "enabled") !== "false";
|
|
563
|
+
}
|
|
564
|
+
function getRecordDomain(record) {
|
|
565
|
+
return getOptionalString(record, "domain") ?? "all";
|
|
566
|
+
}
|
|
567
|
+
function getRecordReferences(record) {
|
|
568
|
+
return getStringList(record, "references");
|
|
569
|
+
}
|
|
570
|
+
function getRecordSection(record, title) {
|
|
571
|
+
return record.sections.get(sectionKey(title));
|
|
572
|
+
}
|
|
573
|
+
function getRecordDecision(record) {
|
|
574
|
+
return getRecordSection(record, "Decision") ?? "";
|
|
575
|
+
}
|
|
576
|
+
function isValidRecordDomain(domain) {
|
|
577
|
+
return domainPattern.test(domain);
|
|
578
|
+
}
|
|
579
|
+
function parseFrontmatter(frontmatter) {
|
|
580
|
+
const result = {};
|
|
581
|
+
const lines = frontmatter.split(/\r?\n/);
|
|
582
|
+
let currentListKey;
|
|
583
|
+
let currentObjectKey;
|
|
584
|
+
for (const line of lines) {
|
|
585
|
+
const listItem = /^\s*-\s*(.*?)\s*$/.exec(line);
|
|
586
|
+
if (listItem !== null && currentListKey !== void 0) {
|
|
587
|
+
const value2 = listItem[1];
|
|
588
|
+
result[currentListKey] = [...asStringList(result[currentListKey]), unquote(value2)];
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
const objectProperty = /^\s+([A-Za-z0-9_.-]+):\s*(.*?)\s*$/.exec(line);
|
|
592
|
+
if (objectProperty !== null && currentObjectKey !== void 0) {
|
|
593
|
+
const [, property, value2] = objectProperty;
|
|
594
|
+
result[currentObjectKey] = {
|
|
595
|
+
...asStringRecord(result[currentObjectKey]),
|
|
596
|
+
[property]: unquote(value2)
|
|
597
|
+
};
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
const keyValue = /^([A-Za-z0-9_-]+):(?:\s*(.*?))?\s*$/.exec(line);
|
|
601
|
+
if (keyValue === null) {
|
|
602
|
+
currentListKey = void 0;
|
|
603
|
+
currentObjectKey = void 0;
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
const [, key, rawValue = ""] = keyValue;
|
|
607
|
+
const value = rawValue.trim();
|
|
608
|
+
if (value.length === 0) {
|
|
609
|
+
result[key] = [];
|
|
610
|
+
currentListKey = key;
|
|
611
|
+
currentObjectKey = key;
|
|
612
|
+
} else {
|
|
613
|
+
result[key] = unquote(value);
|
|
614
|
+
currentListKey = void 0;
|
|
615
|
+
currentObjectKey = void 0;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return result;
|
|
619
|
+
}
|
|
620
|
+
function parseSections(markdown) {
|
|
621
|
+
const sections = /* @__PURE__ */ new Map();
|
|
622
|
+
const lines = markdown.split(/\r?\n/);
|
|
623
|
+
let currentTitle;
|
|
624
|
+
let currentLines = [];
|
|
625
|
+
for (const line of lines) {
|
|
626
|
+
const heading = /^##\s+(.+?)\s*$/.exec(line);
|
|
627
|
+
if (heading !== null) {
|
|
628
|
+
if (currentTitle !== void 0) {
|
|
629
|
+
sections.set(sectionKey(currentTitle), currentLines.join("\n").trim());
|
|
630
|
+
}
|
|
631
|
+
currentTitle = heading[1];
|
|
632
|
+
currentLines = [];
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
if (currentTitle !== void 0) {
|
|
636
|
+
currentLines.push(line);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
if (currentTitle !== void 0) {
|
|
640
|
+
sections.set(sectionKey(currentTitle), currentLines.join("\n").trim());
|
|
641
|
+
}
|
|
642
|
+
return sections;
|
|
643
|
+
}
|
|
644
|
+
async function safeReadDirectory(directory) {
|
|
645
|
+
try {
|
|
646
|
+
return await fs2.readdir(directory, { withFileTypes: true });
|
|
647
|
+
} catch (error) {
|
|
648
|
+
if (isNodeError2(error) && error.code === "ENOENT") {
|
|
649
|
+
return [];
|
|
650
|
+
}
|
|
651
|
+
throw error;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
function validateId(record, errors) {
|
|
655
|
+
const id = getRequiredString(record, "id");
|
|
656
|
+
const status = getRequiredString(record, "status");
|
|
657
|
+
if (id.length === 0) {
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
const allowed = status === "candidate" ? candidateIdPattern : status === "accepted" ? drIdPattern : /^(?:DR|CAND)-\d{4}$/;
|
|
661
|
+
if (!allowed.test(id)) {
|
|
662
|
+
errors.push(`Invalid id "${id}" for status "${status}".`);
|
|
663
|
+
}
|
|
664
|
+
if (!path2.basename(record.filePath).startsWith(id)) {
|
|
665
|
+
errors.push(`Filename must start with id "${id}".`);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
function validateDate(record, field, errors) {
|
|
669
|
+
const value = getOptionalString(record, field);
|
|
670
|
+
if (value !== void 0 && !isoDatePattern.test(value)) {
|
|
671
|
+
errors.push(`Field "${field}" must use YYYY-MM-DD.`);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
function validateBoolean(record, field, errors) {
|
|
675
|
+
const value = getOptionalString(record, field);
|
|
676
|
+
if (value !== void 0 && value !== "true" && value !== "false") {
|
|
677
|
+
errors.push(`Field "${field}" must be true or false.`);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
function validateDomain(record, vocabulary, errors, warnings) {
|
|
681
|
+
if (Object.prototype.hasOwnProperty.call(record.frontmatter, "dimension")) {
|
|
682
|
+
errors.push('Field "dimension" is no longer supported; use "domain".');
|
|
683
|
+
}
|
|
684
|
+
if (!Object.prototype.hasOwnProperty.call(record.frontmatter, "domain")) {
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
const value = record.frontmatter.domain;
|
|
688
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
689
|
+
errors.push('Field "domain" must be a non-empty string.');
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
if (!isValidRecordDomain(value)) {
|
|
693
|
+
errors.push('Field "domain" must be "all" or a lowercase dot-separated kebab-case path.');
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
if (vocabulary.domains.length === 0) {
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
const status = getRequiredString(record, "status");
|
|
700
|
+
const knownDomains = new Set(vocabulary.domains.map((domain) => domain.name));
|
|
701
|
+
knownDomains.add("all");
|
|
702
|
+
if (!knownDomains.has(value)) {
|
|
703
|
+
const message = `Unknown domain "${value}".`;
|
|
704
|
+
if (status === "candidate") {
|
|
705
|
+
warnings.push(message);
|
|
706
|
+
} else {
|
|
707
|
+
errors.push(message);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
function validateTags(record, vocabulary, errors, warnings) {
|
|
712
|
+
const tags = getStringList(record, "tags");
|
|
713
|
+
const status = getRequiredString(record, "status");
|
|
714
|
+
const knownTags = new Set(vocabulary.tags.map((tag) => tag.name));
|
|
715
|
+
for (const tag of tags) {
|
|
716
|
+
if (!knownTags.has(tag)) {
|
|
717
|
+
const message = `Unknown tag "${tag}".`;
|
|
718
|
+
if (status === "candidate") {
|
|
719
|
+
warnings.push(message);
|
|
720
|
+
} else {
|
|
721
|
+
errors.push(message);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
function validateRemovedRetrievalFields(record, errors) {
|
|
727
|
+
if (Object.prototype.hasOwnProperty.call(record.frontmatter, "summary")) {
|
|
728
|
+
errors.push('Field "summary" is no longer supported; short detail uses "title".');
|
|
729
|
+
}
|
|
730
|
+
if (Object.prototype.hasOwnProperty.call(record.frontmatter, "guidance")) {
|
|
731
|
+
errors.push('Field "guidance" is no longer supported; medium detail uses "## Decision".');
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
function getRequiredString(record, key) {
|
|
735
|
+
return getOptionalString(record, key) ?? "";
|
|
736
|
+
}
|
|
737
|
+
function getOptionalString(record, key) {
|
|
738
|
+
const value = record.frontmatter[key];
|
|
739
|
+
return typeof value === "string" ? value : void 0;
|
|
740
|
+
}
|
|
741
|
+
function getStringList(record, key) {
|
|
742
|
+
return asStringList(record.frontmatter[key]);
|
|
743
|
+
}
|
|
744
|
+
function hasFrontmatterValue(frontmatter, key) {
|
|
745
|
+
const value = frontmatter[key];
|
|
746
|
+
if (value === void 0) {
|
|
747
|
+
return false;
|
|
748
|
+
}
|
|
749
|
+
if (typeof value === "string") {
|
|
750
|
+
return value.trim().length > 0;
|
|
751
|
+
}
|
|
752
|
+
if (Array.isArray(value)) {
|
|
753
|
+
return value.length > 0;
|
|
754
|
+
}
|
|
755
|
+
return Object.keys(value).length > 0;
|
|
756
|
+
}
|
|
757
|
+
function asStringList(value) {
|
|
758
|
+
return Array.isArray(value) ? value.filter((item) => item.length > 0) : [];
|
|
759
|
+
}
|
|
760
|
+
function asStringRecord(value) {
|
|
761
|
+
return isStringRecord(value) ? value : {};
|
|
762
|
+
}
|
|
763
|
+
function isStringRecord(value) {
|
|
764
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
765
|
+
}
|
|
766
|
+
function sectionKey(title) {
|
|
767
|
+
return title.trim().toLowerCase().replace(/\s+/g, "-");
|
|
768
|
+
}
|
|
769
|
+
function unquote(value) {
|
|
770
|
+
const trimmed = value.trim();
|
|
771
|
+
if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
|
|
772
|
+
return trimmed.slice(1, -1);
|
|
773
|
+
}
|
|
774
|
+
return trimmed;
|
|
775
|
+
}
|
|
776
|
+
function isNodeError2(error) {
|
|
777
|
+
return error instanceof Error && "code" in error;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// src/core/tags.ts
|
|
781
|
+
var fs3 = __toESM(require("node:fs/promises"));
|
|
782
|
+
var tagNamePattern = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
783
|
+
var domainNamePattern = /^(?:all|[a-z][a-z0-9]*(?:-[a-z0-9]+)*(?:\.[a-z][a-z0-9]*(?:-[a-z0-9]+)*)*)$/;
|
|
784
|
+
async function readTagVocabulary(tagsPath) {
|
|
785
|
+
const markdown = await fs3.readFile(tagsPath, "utf8");
|
|
786
|
+
return parseTagVocabulary(markdown);
|
|
787
|
+
}
|
|
788
|
+
async function acceptVocabularyProposals(tagsPath, proposals) {
|
|
789
|
+
const markdown = await fs3.readFile(tagsPath, "utf8");
|
|
790
|
+
const vocabulary = parseTagVocabulary(markdown);
|
|
791
|
+
let updated = markdown;
|
|
792
|
+
const knownDomains = new Set(vocabulary.domains.map((domain) => domain.name));
|
|
793
|
+
knownDomains.add("all");
|
|
794
|
+
const domainEntries = Object.entries(proposals.domains).filter(([name]) => !knownDomains.has(name)).map(([name, description]) => ({ name, description }));
|
|
795
|
+
updated = appendEntries(updated, "Domains", domainEntries);
|
|
796
|
+
const knownTags = new Set(vocabulary.tags.map((tag) => tag.name));
|
|
797
|
+
const tagEntries = Object.entries(proposals.tags).filter(([name]) => !knownTags.has(name)).map(([name, description]) => ({ name, description }));
|
|
798
|
+
updated = appendEntries(updated, "Tags", tagEntries);
|
|
799
|
+
const updatedVocabulary = parseTagVocabulary(updated);
|
|
800
|
+
if (updatedVocabulary.errors.length > 0) {
|
|
801
|
+
throw new Error(`Vocabulary proposals failed validation: ${updatedVocabulary.errors.join("; ")}`);
|
|
802
|
+
}
|
|
803
|
+
if (updated !== markdown) {
|
|
804
|
+
await fs3.writeFile(tagsPath, updated, "utf8");
|
|
805
|
+
}
|
|
806
|
+
return updatedVocabulary;
|
|
807
|
+
}
|
|
808
|
+
function parseTagVocabulary(markdown) {
|
|
809
|
+
return hasStructuredVocabulary(markdown) ? parseStructuredVocabulary(markdown) : parseLegacyTagVocabulary(markdown);
|
|
810
|
+
}
|
|
811
|
+
function parseStructuredVocabulary(markdown) {
|
|
812
|
+
const lines = markdown.split(/\r?\n/);
|
|
813
|
+
const domains = [];
|
|
814
|
+
const tags = [];
|
|
815
|
+
const errors = [];
|
|
816
|
+
let currentSection;
|
|
817
|
+
let current;
|
|
818
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
819
|
+
const line = lines[index];
|
|
820
|
+
const sectionHeading = /^##\s+(.+?)\s*$/.exec(line);
|
|
821
|
+
if (sectionHeading !== null) {
|
|
822
|
+
finishCurrent(current, domains, tags);
|
|
823
|
+
current = void 0;
|
|
824
|
+
const section = sectionKey2(sectionHeading[1]);
|
|
825
|
+
currentSection = section === "domains" || section === "tags" ? section : void 0;
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
const entryHeading = /^###\s+(.+?)\s*$/.exec(line);
|
|
829
|
+
if (entryHeading !== null && currentSection !== void 0) {
|
|
830
|
+
finishCurrent(current, domains, tags);
|
|
831
|
+
const name = entryHeading[1].trim();
|
|
832
|
+
current = {
|
|
833
|
+
section: currentSection,
|
|
834
|
+
name,
|
|
835
|
+
line: index + 1,
|
|
836
|
+
descriptionLines: []
|
|
837
|
+
};
|
|
838
|
+
if (currentSection === "domains" && !domainNamePattern.test(name)) {
|
|
839
|
+
errors.push(`Invalid domain "${name}" at line ${index + 1}; use "all" or lowercase dot-separated kebab-case.`);
|
|
840
|
+
}
|
|
841
|
+
if (currentSection === "tags" && !tagNamePattern.test(name)) {
|
|
842
|
+
errors.push(`Invalid tag "${name}" at line ${index + 1}; use lowercase kebab-case.`);
|
|
843
|
+
}
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
if (current !== void 0) {
|
|
847
|
+
current.descriptionLines.push(line);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
finishCurrent(current, domains, tags);
|
|
851
|
+
addDuplicateErrors(domains, "domain", errors);
|
|
852
|
+
addDuplicateErrors(tags, "tag", errors);
|
|
853
|
+
return { domains, tags, errors };
|
|
854
|
+
}
|
|
855
|
+
function parseLegacyTagVocabulary(markdown) {
|
|
856
|
+
const lines = markdown.split(/\r?\n/);
|
|
857
|
+
const tags = [];
|
|
858
|
+
const errors = [];
|
|
859
|
+
let current;
|
|
860
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
861
|
+
const line = lines[index];
|
|
862
|
+
const heading = /^##\s+(.+?)\s*$/.exec(line);
|
|
863
|
+
if (heading !== null) {
|
|
864
|
+
finishCurrent(current, [], tags);
|
|
865
|
+
const name = heading[1].trim();
|
|
866
|
+
current = {
|
|
867
|
+
section: "tags",
|
|
868
|
+
name,
|
|
869
|
+
line: index + 1,
|
|
870
|
+
descriptionLines: []
|
|
871
|
+
};
|
|
872
|
+
if (!tagNamePattern.test(name)) {
|
|
873
|
+
errors.push(`Invalid tag "${name}" at line ${index + 1}; use lowercase kebab-case.`);
|
|
874
|
+
}
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
if (current !== void 0) {
|
|
878
|
+
current.descriptionLines.push(line);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
finishCurrent(current, [], tags);
|
|
882
|
+
addDuplicateErrors(tags, "tag", errors);
|
|
883
|
+
return { domains: [], tags, errors };
|
|
884
|
+
}
|
|
885
|
+
function finishDefinition(definition) {
|
|
886
|
+
return {
|
|
887
|
+
name: definition.name,
|
|
888
|
+
description: definition.descriptionLines.join("\n").trim(),
|
|
889
|
+
line: definition.line
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
function finishCurrent(current, domains, tags) {
|
|
893
|
+
if (current === void 0) {
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
if (current.section === "domains") {
|
|
897
|
+
domains.push(finishDefinition(current));
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
tags.push(finishDefinition(current));
|
|
901
|
+
}
|
|
902
|
+
function addDuplicateErrors(definitions, label, errors) {
|
|
903
|
+
const seen = /* @__PURE__ */ new Set();
|
|
904
|
+
for (const definition of definitions) {
|
|
905
|
+
if (seen.has(definition.name)) {
|
|
906
|
+
errors.push(`Duplicate ${label} "${definition.name}" at line ${definition.line}.`);
|
|
907
|
+
}
|
|
908
|
+
seen.add(definition.name);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
function hasStructuredVocabulary(markdown) {
|
|
912
|
+
return /^##\s+(?:Domains|Tags)\s*$/im.test(markdown);
|
|
913
|
+
}
|
|
914
|
+
function appendEntries(markdown, sectionTitle, entries) {
|
|
915
|
+
if (entries.length === 0) {
|
|
916
|
+
return markdown;
|
|
917
|
+
}
|
|
918
|
+
const lines = markdown.trimEnd().split(/\r?\n/);
|
|
919
|
+
const sectionIndex = lines.findIndex((line) => sectionKey2(line.replace(/^##\s+/, "")) === sectionKey2(sectionTitle));
|
|
920
|
+
const rendered = entries.flatMap((entry) => [
|
|
921
|
+
`### ${entry.name}`,
|
|
922
|
+
"",
|
|
923
|
+
entry.description.trim().length === 0 ? `TODO: describe ${entry.name}.` : entry.description.trim(),
|
|
924
|
+
""
|
|
925
|
+
]);
|
|
926
|
+
if (sectionIndex === -1) {
|
|
927
|
+
return [
|
|
928
|
+
...lines,
|
|
929
|
+
"",
|
|
930
|
+
`## ${sectionTitle}`,
|
|
931
|
+
"",
|
|
932
|
+
...rendered
|
|
933
|
+
].join("\n");
|
|
934
|
+
}
|
|
935
|
+
const nextSectionIndex = lines.findIndex((line, index) => index > sectionIndex && /^##\s+/.test(line));
|
|
936
|
+
const insertIndex = nextSectionIndex === -1 ? lines.length : nextSectionIndex;
|
|
937
|
+
const prefix = lines.slice(0, insertIndex);
|
|
938
|
+
const suffix = lines.slice(insertIndex);
|
|
939
|
+
return [
|
|
940
|
+
...prefix,
|
|
941
|
+
...prefix[prefix.length - 1]?.trim() === "" ? [] : [""],
|
|
942
|
+
...rendered,
|
|
943
|
+
...suffix
|
|
944
|
+
].join("\n");
|
|
945
|
+
}
|
|
946
|
+
function sectionKey2(title) {
|
|
947
|
+
return title.trim().toLowerCase().replace(/\s+/g, "-");
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// src/core/candidates.ts
|
|
951
|
+
async function createCandidate(paths, vocabulary, input) {
|
|
952
|
+
const knownDomainNames = new Set(vocabulary.domains.map((domain) => domain.name));
|
|
953
|
+
knownDomainNames.add("all");
|
|
954
|
+
const knownTagNames = new Set(vocabulary.tags.map((tag) => tag.name));
|
|
955
|
+
const knownTags = input.tags.filter((tag) => knownTagNames.has(tag));
|
|
956
|
+
const proposedTags = input.tags.filter((tag) => !knownTagNames.has(tag));
|
|
957
|
+
const proposedDomains = knownDomainNames.has(input.domain) || vocabulary.domains.length === 0 ? [] : [input.domain];
|
|
958
|
+
const id = await nextRecordId(paths, "CAND");
|
|
959
|
+
const filePath = path3.join(decisionRecordDirectory(paths, "candidate"), `${id}-${slugify(input.title)}.md`);
|
|
960
|
+
const frontmatter = {
|
|
961
|
+
id,
|
|
962
|
+
title: input.title,
|
|
963
|
+
status: "candidate",
|
|
964
|
+
domain: input.domain,
|
|
965
|
+
created: input.created,
|
|
966
|
+
created_by: input.author,
|
|
967
|
+
tags: knownTags
|
|
968
|
+
};
|
|
969
|
+
if (proposedTags.length > 0) {
|
|
970
|
+
frontmatter.proposed_tags = Object.fromEntries(proposedTags.map((tag) => [
|
|
971
|
+
tag,
|
|
972
|
+
input.proposedTagDescriptions?.[tag] ?? `TODO: describe ${tag}.`
|
|
973
|
+
]));
|
|
974
|
+
}
|
|
975
|
+
if (proposedDomains.length > 0) {
|
|
976
|
+
frontmatter.proposed_domains = {
|
|
977
|
+
[input.domain]: input.proposedDomainDescription ?? `TODO: describe ${input.domain}.`
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
if (input.affectedFiles.length > 0) {
|
|
981
|
+
frontmatter.affected_files = input.affectedFiles;
|
|
982
|
+
}
|
|
983
|
+
if (input.references.length > 0) {
|
|
984
|
+
frontmatter.references = input.references;
|
|
985
|
+
}
|
|
986
|
+
const markdown = [
|
|
987
|
+
renderFrontmatter(frontmatter),
|
|
988
|
+
"",
|
|
989
|
+
"## Decision",
|
|
990
|
+
"",
|
|
991
|
+
input.decision,
|
|
992
|
+
...input.appendix === void 0 ? [] : ["", "## Appendix", "", input.appendix],
|
|
993
|
+
""
|
|
994
|
+
].join("\n");
|
|
995
|
+
await fs4.writeFile(filePath, markdown, "utf8");
|
|
996
|
+
return {
|
|
997
|
+
record: await readDecisionRecord(filePath, "candidate"),
|
|
998
|
+
knownTags,
|
|
999
|
+
proposedTags,
|
|
1000
|
+
proposedDomains
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
async function findCandidate(paths, id) {
|
|
1004
|
+
const records = await listDecisionRecords(paths);
|
|
1005
|
+
return records.find((record) => getRecordId(record) === id);
|
|
1006
|
+
}
|
|
1007
|
+
async function acceptCandidate(paths, vocabulary, id, updated) {
|
|
1008
|
+
const candidate = await requireCandidateInFolder(paths, id, "candidate");
|
|
1009
|
+
return acceptCandidateRecord(paths, vocabulary, candidate, updated);
|
|
1010
|
+
}
|
|
1011
|
+
async function promoteDecisionRecord(paths, vocabulary, id, from, updated) {
|
|
1012
|
+
const record = await requireRecordInFolder(paths, id, from);
|
|
1013
|
+
if (getRecordId(record).startsWith("CAND-") || getString(record, "kind") === "dr") {
|
|
1014
|
+
return acceptCandidateRecord(paths, vocabulary, record, updated);
|
|
1015
|
+
}
|
|
1016
|
+
const frontmatter = {
|
|
1017
|
+
...record.frontmatter,
|
|
1018
|
+
status: "accepted",
|
|
1019
|
+
updated,
|
|
1020
|
+
author: getString(record, "author") || getString(record, "created_by") || "agent"
|
|
1021
|
+
};
|
|
1022
|
+
delete frontmatter.rejection_reason;
|
|
1023
|
+
delete frontmatter.retired_by;
|
|
1024
|
+
delete frontmatter.retirement_reason;
|
|
1025
|
+
delete frontmatter.summary;
|
|
1026
|
+
delete frontmatter.guidance;
|
|
1027
|
+
const markdown = `${renderFrontmatter(frontmatter)}
|
|
1028
|
+
${record.body.trimStart()}`;
|
|
1029
|
+
const to = path3.join(decisionRecordDirectory(paths, "accepted"), path3.basename(record.filePath));
|
|
1030
|
+
await fs4.writeFile(to, markdown.endsWith("\n") ? markdown : `${markdown}
|
|
1031
|
+
`, "utf8");
|
|
1032
|
+
await fs4.unlink(record.filePath);
|
|
1033
|
+
const accepted = await readDecisionRecord(to, "accepted");
|
|
1034
|
+
const validation = validateDecisionRecord(accepted, vocabulary);
|
|
1035
|
+
if (validation.errors.length > 0) {
|
|
1036
|
+
throw new Error(`Promoted DR failed validation: ${validation.errors.join("; ")}`);
|
|
1037
|
+
}
|
|
1038
|
+
return { from: record.filePath, to, record: accepted };
|
|
1039
|
+
}
|
|
1040
|
+
async function deleteDecisionRecord(paths, id, from) {
|
|
1041
|
+
const record = await requireRecordInFolder(paths, id, from);
|
|
1042
|
+
await fs4.unlink(record.filePath);
|
|
1043
|
+
return {
|
|
1044
|
+
from: record.filePath,
|
|
1045
|
+
id: getRecordId(record),
|
|
1046
|
+
title: getRecordTitle(record),
|
|
1047
|
+
status: from
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
async function retireDecisionRecord(paths, id, retiredBy) {
|
|
1051
|
+
const record = await requireRecordInFolder(paths, id, "accepted");
|
|
1052
|
+
const frontmatter = {
|
|
1053
|
+
...record.frontmatter,
|
|
1054
|
+
status: "retired",
|
|
1055
|
+
...retiredBy === void 0 ? {} : { retired_by: retiredBy }
|
|
1056
|
+
};
|
|
1057
|
+
delete frontmatter.summary;
|
|
1058
|
+
delete frontmatter.guidance;
|
|
1059
|
+
const markdown = `${renderFrontmatter(frontmatter)}
|
|
1060
|
+
${record.body.trimStart()}`;
|
|
1061
|
+
const to = path3.join(decisionRecordDirectory(paths, "retired"), path3.basename(record.filePath));
|
|
1062
|
+
await fs4.writeFile(to, markdown.endsWith("\n") ? markdown : `${markdown}
|
|
1063
|
+
`, "utf8");
|
|
1064
|
+
await fs4.unlink(record.filePath);
|
|
1065
|
+
return {
|
|
1066
|
+
from: record.filePath,
|
|
1067
|
+
to,
|
|
1068
|
+
record: await readDecisionRecord(to, "retired")
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
async function setDecisionRecordEnabled(paths, id, enabled) {
|
|
1072
|
+
const record = await requireRecordInFolder(paths, id, "accepted");
|
|
1073
|
+
const frontmatter = {
|
|
1074
|
+
...record.frontmatter,
|
|
1075
|
+
enabled: enabled ? "true" : "false"
|
|
1076
|
+
};
|
|
1077
|
+
delete frontmatter.summary;
|
|
1078
|
+
delete frontmatter.guidance;
|
|
1079
|
+
const markdown = `${renderFrontmatter(frontmatter)}
|
|
1080
|
+
${record.body.trimStart()}`;
|
|
1081
|
+
await fs4.writeFile(record.filePath, markdown.endsWith("\n") ? markdown : `${markdown}
|
|
1082
|
+
`, "utf8");
|
|
1083
|
+
return {
|
|
1084
|
+
from: record.filePath,
|
|
1085
|
+
to: record.filePath,
|
|
1086
|
+
record: await readDecisionRecord(record.filePath, "accepted")
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
async function acceptCandidateRecord(paths, vocabulary, candidate, updated) {
|
|
1090
|
+
const kind = getString(candidate, "kind");
|
|
1091
|
+
if (kind.length > 0 && kind !== "dr") {
|
|
1092
|
+
throw new Error(`Candidate ${getRecordId(candidate)} is kind "${kind}"; only kind "dr" can be accepted as precedent.`);
|
|
1093
|
+
}
|
|
1094
|
+
const proposedTags = getProposalRecord(candidate, "proposed_tags");
|
|
1095
|
+
const proposedDomains = getProposalRecord(candidate, "proposed_domains");
|
|
1096
|
+
const acceptedVocabulary = Object.keys(proposedTags).length === 0 && Object.keys(proposedDomains).length === 0 ? vocabulary : await acceptVocabularyProposals(paths.tags, {
|
|
1097
|
+
tags: proposedTags,
|
|
1098
|
+
domains: proposedDomains
|
|
1099
|
+
});
|
|
1100
|
+
const acceptedTags = uniqueStrings([...getStringList2(candidate, "tags"), ...Object.keys(proposedTags)]);
|
|
1101
|
+
const nextId = await nextRecordId(paths, "DR");
|
|
1102
|
+
const frontmatter = {
|
|
1103
|
+
...candidate.frontmatter,
|
|
1104
|
+
id: nextId,
|
|
1105
|
+
status: "accepted",
|
|
1106
|
+
updated,
|
|
1107
|
+
author: getString(candidate, "author") || getString(candidate, "created_by") || "agent",
|
|
1108
|
+
tags: acceptedTags
|
|
1109
|
+
};
|
|
1110
|
+
delete frontmatter.kind;
|
|
1111
|
+
delete frontmatter.created_by;
|
|
1112
|
+
delete frontmatter.proposed_tags;
|
|
1113
|
+
delete frontmatter.proposed_domains;
|
|
1114
|
+
delete frontmatter.rejection_reason;
|
|
1115
|
+
delete frontmatter.retired_by;
|
|
1116
|
+
delete frontmatter.retirement_reason;
|
|
1117
|
+
delete frontmatter.summary;
|
|
1118
|
+
delete frontmatter.guidance;
|
|
1119
|
+
const acceptedMarkdown = `${renderFrontmatter(frontmatter)}
|
|
1120
|
+
${candidate.body.trimStart()}`;
|
|
1121
|
+
const to = path3.join(decisionRecordDirectory(paths, "accepted"), `${nextId}-${slugify(getRecordTitle(candidate))}.md`);
|
|
1122
|
+
await fs4.writeFile(to, acceptedMarkdown.endsWith("\n") ? acceptedMarkdown : `${acceptedMarkdown}
|
|
1123
|
+
`, "utf8");
|
|
1124
|
+
await fs4.unlink(candidate.filePath);
|
|
1125
|
+
const record = await readDecisionRecord(to, "accepted");
|
|
1126
|
+
const validation = validateDecisionRecord(record, acceptedVocabulary);
|
|
1127
|
+
if (validation.errors.length > 0) {
|
|
1128
|
+
throw new Error(`Accepted candidate failed validation: ${validation.errors.join("; ")}`);
|
|
1129
|
+
}
|
|
1130
|
+
return { from: candidate.filePath, to, record };
|
|
1131
|
+
}
|
|
1132
|
+
async function rejectCandidate(paths, id, reason) {
|
|
1133
|
+
return moveCandidate(paths, id, "rejected", reason === void 0 ? {} : { rejection_reason: reason });
|
|
1134
|
+
}
|
|
1135
|
+
async function retireCandidate(paths, id, retiredBy) {
|
|
1136
|
+
return moveCandidate(paths, id, "retired", retiredBy === void 0 ? {} : { retired_by: retiredBy });
|
|
1137
|
+
}
|
|
1138
|
+
function renderFrontmatter(frontmatter) {
|
|
1139
|
+
const lines = ["---"];
|
|
1140
|
+
for (const [key, value] of Object.entries(frontmatter)) {
|
|
1141
|
+
if (Array.isArray(value)) {
|
|
1142
|
+
lines.push(`${key}:`);
|
|
1143
|
+
for (const item of value) {
|
|
1144
|
+
lines.push(` - ${item}`);
|
|
1145
|
+
}
|
|
1146
|
+
continue;
|
|
1147
|
+
}
|
|
1148
|
+
if (isStringRecord2(value)) {
|
|
1149
|
+
lines.push(`${key}:`);
|
|
1150
|
+
for (const [nestedKey, nestedValue] of Object.entries(value)) {
|
|
1151
|
+
lines.push(` ${nestedKey}: ${nestedValue}`);
|
|
1152
|
+
}
|
|
1153
|
+
continue;
|
|
1154
|
+
}
|
|
1155
|
+
lines.push(`${key}: ${value}`);
|
|
1156
|
+
}
|
|
1157
|
+
lines.push("---");
|
|
1158
|
+
return lines.join("\n");
|
|
1159
|
+
}
|
|
1160
|
+
async function moveCandidate(paths, id, status, frontmatterPatch) {
|
|
1161
|
+
const candidate = await requireCandidateInFolder(paths, id, "candidate");
|
|
1162
|
+
const frontmatter = {
|
|
1163
|
+
...candidate.frontmatter,
|
|
1164
|
+
status,
|
|
1165
|
+
...frontmatterPatch
|
|
1166
|
+
};
|
|
1167
|
+
delete frontmatter.summary;
|
|
1168
|
+
delete frontmatter.guidance;
|
|
1169
|
+
const markdown = `${renderFrontmatter(frontmatter)}
|
|
1170
|
+
${candidate.body.trimStart()}`;
|
|
1171
|
+
const to = path3.join(decisionRecordDirectory(paths, status), path3.basename(candidate.filePath));
|
|
1172
|
+
await fs4.writeFile(to, markdown.endsWith("\n") ? markdown : `${markdown}
|
|
1173
|
+
`, "utf8");
|
|
1174
|
+
await fs4.unlink(candidate.filePath);
|
|
1175
|
+
return {
|
|
1176
|
+
from: candidate.filePath,
|
|
1177
|
+
to,
|
|
1178
|
+
record: await readDecisionRecord(to, status)
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
async function requireCandidateInFolder(paths, id, folder) {
|
|
1182
|
+
return requireRecordInFolder(paths, id, folder);
|
|
1183
|
+
}
|
|
1184
|
+
async function requireRecordInFolder(paths, id, folder) {
|
|
1185
|
+
const records = await listDecisionRecords(paths, folder);
|
|
1186
|
+
const candidate = records.find((record) => getRecordId(record) === id);
|
|
1187
|
+
if (candidate === void 0) {
|
|
1188
|
+
throw new Error(`No ${folder} DR found with id "${id}".`);
|
|
1189
|
+
}
|
|
1190
|
+
return candidate;
|
|
1191
|
+
}
|
|
1192
|
+
async function nextRecordId(paths, prefix) {
|
|
1193
|
+
const records = await listDecisionRecords(paths);
|
|
1194
|
+
let max = 0;
|
|
1195
|
+
for (const record of records) {
|
|
1196
|
+
const id = getRecordId(record);
|
|
1197
|
+
if (!id.startsWith(`${prefix}-`)) {
|
|
1198
|
+
continue;
|
|
1199
|
+
}
|
|
1200
|
+
const value = Number(id.slice(prefix.length + 1));
|
|
1201
|
+
if (Number.isInteger(value)) {
|
|
1202
|
+
max = Math.max(max, value);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
return `${prefix}-${String(max + 1).padStart(4, "0")}`;
|
|
1206
|
+
}
|
|
1207
|
+
function getString(record, key) {
|
|
1208
|
+
const value = record.frontmatter[key];
|
|
1209
|
+
return typeof value === "string" ? value : "";
|
|
1210
|
+
}
|
|
1211
|
+
function getStringList2(record, key) {
|
|
1212
|
+
const value = record.frontmatter[key];
|
|
1213
|
+
return Array.isArray(value) ? value : [];
|
|
1214
|
+
}
|
|
1215
|
+
function getProposalRecord(record, key) {
|
|
1216
|
+
const value = record.frontmatter[key];
|
|
1217
|
+
if (isStringRecord2(value)) {
|
|
1218
|
+
return { ...value };
|
|
1219
|
+
}
|
|
1220
|
+
if (Array.isArray(value)) {
|
|
1221
|
+
return Object.fromEntries(value.map((item) => [item, `TODO: describe ${item}.`]));
|
|
1222
|
+
}
|
|
1223
|
+
return {};
|
|
1224
|
+
}
|
|
1225
|
+
function uniqueStrings(values) {
|
|
1226
|
+
return [...new Set(values)];
|
|
1227
|
+
}
|
|
1228
|
+
function slugify(title) {
|
|
1229
|
+
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1230
|
+
return slug.length === 0 ? "candidate" : slug;
|
|
1231
|
+
}
|
|
1232
|
+
function isStringRecord2(value) {
|
|
1233
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
// src/core/validation.ts
|
|
1237
|
+
var fs5 = __toESM(require("node:fs/promises"));
|
|
1238
|
+
var path4 = __toESM(require("node:path"));
|
|
1239
|
+
async function validateStore(paths) {
|
|
1240
|
+
const vocabulary = await readTagVocabulary(paths.tags);
|
|
1241
|
+
const records = await listDecisionRecords(paths);
|
|
1242
|
+
const validationResults = await Promise.all(records.map(async (record) => {
|
|
1243
|
+
const validation = validateDecisionRecord(record, vocabulary);
|
|
1244
|
+
const referenceWarnings = await brokenReferenceWarnings(paths, record);
|
|
1245
|
+
return {
|
|
1246
|
+
...validation,
|
|
1247
|
+
warnings: [...validation.warnings, ...referenceWarnings]
|
|
1248
|
+
};
|
|
1249
|
+
}));
|
|
1250
|
+
return {
|
|
1251
|
+
validationResults,
|
|
1252
|
+
vocabularyErrors: vocabulary.errors
|
|
1253
|
+
};
|
|
1254
|
+
}
|
|
1255
|
+
async function brokenReferenceWarnings(paths, record) {
|
|
1256
|
+
const warnings = [];
|
|
1257
|
+
for (const reference of getRecordReferences(record)) {
|
|
1258
|
+
if (reference.includes("://")) {
|
|
1259
|
+
continue;
|
|
1260
|
+
}
|
|
1261
|
+
const filePart = reference.split("#")[0];
|
|
1262
|
+
if (filePart.length === 0) {
|
|
1263
|
+
continue;
|
|
1264
|
+
}
|
|
1265
|
+
try {
|
|
1266
|
+
await fs5.access(path4.resolve(paths.root, filePart));
|
|
1267
|
+
} catch (error) {
|
|
1268
|
+
if (isNodeError3(error) && error.code === "ENOENT") {
|
|
1269
|
+
warnings.push(`Reference target does not exist: ${reference}`);
|
|
1270
|
+
continue;
|
|
1271
|
+
}
|
|
1272
|
+
throw error;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
return warnings;
|
|
1276
|
+
}
|
|
1277
|
+
function isNodeError3(error) {
|
|
1278
|
+
return error instanceof Error && "code" in error;
|
|
1279
|
+
}
|
|
1280
|
+
function validationErrorCount(result) {
|
|
1281
|
+
return result.vocabularyErrors.length + result.validationResults.reduce((sum, item) => sum + item.errors.length, 0);
|
|
1282
|
+
}
|
|
1283
|
+
function validationWarningCount(result) {
|
|
1284
|
+
return result.validationResults.reduce((sum, item) => sum + item.warnings.length, 0);
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// src/main.ts
|
|
1288
|
+
var usage = `CodeSteward CLI
|
|
1289
|
+
|
|
1290
|
+
Usage:
|
|
1291
|
+
codesteward [--cwd <path>] [--quiet] [--no-session-log] <command>
|
|
1292
|
+
|
|
1293
|
+
Commands:
|
|
1294
|
+
init Create or update the project-local .codesteward store at an explicit root
|
|
1295
|
+
update Update installed skill files for the discovered or explicit project root
|
|
1296
|
+
status Report store health, counts, and validation state
|
|
1297
|
+
bootstrap Run an LLM-backed bootstrap that creates candidates via the CLI
|
|
1298
|
+
tags List known domains and tags from .codesteward/tags.md
|
|
1299
|
+
dr retrieve Retrieve visible accepted DRs by domain and optional tag
|
|
1300
|
+
dr get Cat DR markdown files by id
|
|
1301
|
+
dr list List DRs by status or tag
|
|
1302
|
+
dr enable Include an accepted DR in retrieval results
|
|
1303
|
+
dr disable Suppress an accepted DR from retrieval results
|
|
1304
|
+
dr retire Move an accepted DR to retired history
|
|
1305
|
+
dr promote Move a rejected or retired DR back to accepted precedent
|
|
1306
|
+
dr delete Remove a rejected or retired DR file from disk
|
|
1307
|
+
candidate Create, list, show, accept, reject, or retire candidates
|
|
1308
|
+
help Show this help
|
|
1309
|
+
`;
|
|
1310
|
+
var defaultRecordRenderOptions = {
|
|
1311
|
+
showStatus: true,
|
|
1312
|
+
showEnabled: true,
|
|
1313
|
+
showPath: true
|
|
1314
|
+
};
|
|
1315
|
+
var retrieveRecordRenderOptions = {
|
|
1316
|
+
showStatus: false,
|
|
1317
|
+
showEnabled: false,
|
|
1318
|
+
showPath: false
|
|
1319
|
+
};
|
|
1320
|
+
async function main(argv, io) {
|
|
1321
|
+
const parsed = parseArguments(argv, io.cwd());
|
|
1322
|
+
if (parsed.command.length === 0 || parsed.command[0] === "help" || parsed.command[0] === "--help") {
|
|
1323
|
+
write(io.stdout, usage);
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
const [command, ...commandArgs] = parsed.command;
|
|
1327
|
+
if (command === "init") {
|
|
1328
|
+
await runInit(parsed, commandArgs, io);
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
if (command === "tags") {
|
|
1332
|
+
await runTags(parsed, commandArgs, io);
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
if (command === "status") {
|
|
1336
|
+
await runStatus(parsed, commandArgs, io);
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
if (command === "update") {
|
|
1340
|
+
await runUpdate(parsed, commandArgs, io);
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
if (command === "bootstrap") {
|
|
1344
|
+
await runBootstrap(parsed, commandArgs, io);
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
if (command === "dr") {
|
|
1348
|
+
await runDr(parsed, commandArgs, io);
|
|
1349
|
+
return;
|
|
1350
|
+
}
|
|
1351
|
+
if (command === "candidate") {
|
|
1352
|
+
await runCandidate(parsed, commandArgs, io);
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
write(io.stderr, `Unknown command: ${command}
|
|
1356
|
+
|
|
1357
|
+
${usage}`);
|
|
1358
|
+
io.exitCode = 64;
|
|
1359
|
+
}
|
|
1360
|
+
async function runBootstrap(options, args, io) {
|
|
1361
|
+
const parsed = parseBootstrapArgs(args, io);
|
|
1362
|
+
if (parsed === void 0) {
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
const paths = await requireStore(options.cwd, io);
|
|
1366
|
+
if (paths === void 0) {
|
|
1367
|
+
return;
|
|
1368
|
+
}
|
|
1369
|
+
const prompt = bootstrapPrompt(paths);
|
|
1370
|
+
const command = bootstrapCommand(parsed.provider, paths.root, prompt);
|
|
1371
|
+
try {
|
|
1372
|
+
if (!options.quiet) {
|
|
1373
|
+
write(io.stdout, `Starting bootstrap with ${parsed.provider}: ${formatBootstrapCommand(command)}
|
|
1374
|
+
`);
|
|
1375
|
+
}
|
|
1376
|
+
await runBootstrapCommand(command, paths.root, options.quiet ? void 0 : io);
|
|
1377
|
+
if (!options.quiet) {
|
|
1378
|
+
write(io.stdout, `Bootstrap completed with ${parsed.provider}.
|
|
1379
|
+
`);
|
|
1380
|
+
}
|
|
1381
|
+
} catch (error) {
|
|
1382
|
+
writeError(error, io);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
async function runCandidate(options, args, io) {
|
|
1386
|
+
const [subcommand, ...subcommandArgs] = args;
|
|
1387
|
+
if (subcommand === "create") {
|
|
1388
|
+
await runCandidateCreate(options, subcommandArgs, io);
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
if (subcommand === "list") {
|
|
1392
|
+
await runCandidateList(options, subcommandArgs, io);
|
|
1393
|
+
return;
|
|
1394
|
+
}
|
|
1395
|
+
if (subcommand === "show") {
|
|
1396
|
+
await runCandidateShow(options, subcommandArgs, io);
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
if (subcommand === "accept") {
|
|
1400
|
+
await runCandidateAccept(options, subcommandArgs, io);
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
if (subcommand === "reject") {
|
|
1404
|
+
await runCandidateReject(options, subcommandArgs, io);
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
if (subcommand === "retire") {
|
|
1408
|
+
await runCandidateRetire(options, subcommandArgs, io);
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
write(io.stderr, "Usage: codesteward candidate (create | list | show | accept | reject | retire)\n");
|
|
1412
|
+
io.exitCode = 64;
|
|
1413
|
+
}
|
|
1414
|
+
async function runCandidateCreate(options, args, io) {
|
|
1415
|
+
const parsed = parseCandidateCreateArgs(args, io);
|
|
1416
|
+
if (parsed === void 0) {
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
const paths = await requireStore(options.cwd, io);
|
|
1420
|
+
if (paths === void 0) {
|
|
1421
|
+
return;
|
|
1422
|
+
}
|
|
1423
|
+
const vocabulary = await readTagVocabulary(paths.tags);
|
|
1424
|
+
const result = await createCandidate(paths, vocabulary, {
|
|
1425
|
+
...parsed,
|
|
1426
|
+
author: defaultAuthor(),
|
|
1427
|
+
created: today()
|
|
1428
|
+
});
|
|
1429
|
+
if (options.quiet) {
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1432
|
+
write(io.stdout, `Created ${getRecordId(result.record)} ${getRecordTitle(result.record)}
|
|
1433
|
+
`);
|
|
1434
|
+
write(io.stdout, `Path: ${formatRecordPath(paths, result.record)}
|
|
1435
|
+
`);
|
|
1436
|
+
if (result.proposedTags.length > 0) {
|
|
1437
|
+
write(io.stdout, `Proposed tags: ${result.proposedTags.join(", ")}
|
|
1438
|
+
`);
|
|
1439
|
+
}
|
|
1440
|
+
if (result.proposedDomains.length > 0) {
|
|
1441
|
+
write(io.stdout, `Proposed domains: ${result.proposedDomains.join(", ")}
|
|
1442
|
+
`);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
async function runCandidateList(options, args, io) {
|
|
1446
|
+
const parsed = parseCandidateListArgs(args, io);
|
|
1447
|
+
if (parsed === void 0) {
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
const paths = await requireStore(options.cwd, io);
|
|
1451
|
+
if (paths === void 0) {
|
|
1452
|
+
return;
|
|
1453
|
+
}
|
|
1454
|
+
const records = await listDecisionRecords(paths, parsed.status);
|
|
1455
|
+
const matches = records.filter((record) => getRecordId(record).startsWith("CAND-"));
|
|
1456
|
+
if (options.quiet) {
|
|
1457
|
+
return;
|
|
1458
|
+
}
|
|
1459
|
+
for (const record of matches) {
|
|
1460
|
+
write(io.stdout, `${getRecordId(record)} ${getRecordTitle(record)} (${getRecordStatus(record)})
|
|
1461
|
+
`);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
async function runCandidateShow(options, args, io) {
|
|
1465
|
+
if (args.length !== 1) {
|
|
1466
|
+
write(io.stderr, "Usage: codesteward candidate show <id>\n");
|
|
1467
|
+
io.exitCode = 64;
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
const paths = await requireStore(options.cwd, io);
|
|
1471
|
+
if (paths === void 0) {
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
const record = await findCandidate(paths, args[0]);
|
|
1475
|
+
if (record === void 0) {
|
|
1476
|
+
write(io.stderr, `No candidate found with id "${args[0]}".
|
|
1477
|
+
`);
|
|
1478
|
+
io.exitCode = 1;
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
if (!options.quiet) {
|
|
1482
|
+
renderRecord(record, "full", paths, io);
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
async function runCandidateAccept(options, args, io) {
|
|
1486
|
+
if (args.length !== 1) {
|
|
1487
|
+
write(io.stderr, "Usage: codesteward candidate accept <id>\n");
|
|
1488
|
+
io.exitCode = 64;
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
1491
|
+
const paths = await requireStore(options.cwd, io);
|
|
1492
|
+
if (paths === void 0) {
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
try {
|
|
1496
|
+
const result = await acceptCandidate(paths, await readTagVocabulary(paths.tags), args[0], today());
|
|
1497
|
+
if (!options.quiet) {
|
|
1498
|
+
write(io.stdout, `Accepted ${args[0]} as ${getRecordId(result.record)}
|
|
1499
|
+
`);
|
|
1500
|
+
write(io.stdout, `Path: ${formatRecordPath(paths, result.record)}
|
|
1501
|
+
`);
|
|
1502
|
+
}
|
|
1503
|
+
} catch (error) {
|
|
1504
|
+
writeError(error, io);
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
async function runCandidateReject(options, args, io) {
|
|
1508
|
+
const parsed = parseCandidateRejectArgs(args, io);
|
|
1509
|
+
if (parsed === void 0) {
|
|
1510
|
+
return;
|
|
1511
|
+
}
|
|
1512
|
+
const paths = await requireStore(options.cwd, io);
|
|
1513
|
+
if (paths === void 0) {
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1516
|
+
try {
|
|
1517
|
+
const result = await rejectCandidate(paths, parsed.id, parsed.reason);
|
|
1518
|
+
if (!options.quiet) {
|
|
1519
|
+
write(io.stdout, `Rejected ${parsed.id}
|
|
1520
|
+
`);
|
|
1521
|
+
write(io.stdout, `Path: ${formatRecordPath(paths, result.record)}
|
|
1522
|
+
`);
|
|
1523
|
+
}
|
|
1524
|
+
} catch (error) {
|
|
1525
|
+
writeError(error, io);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
async function runCandidateRetire(options, args, io) {
|
|
1529
|
+
const parsed = parseCandidateRetireArgs(args, io);
|
|
1530
|
+
if (parsed === void 0) {
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
const paths = await requireStore(options.cwd, io);
|
|
1534
|
+
if (paths === void 0) {
|
|
1535
|
+
return;
|
|
1536
|
+
}
|
|
1537
|
+
try {
|
|
1538
|
+
const result = await retireCandidate(paths, parsed.id, parsed.by);
|
|
1539
|
+
if (!options.quiet) {
|
|
1540
|
+
write(io.stdout, `Retired ${parsed.id}${parsed.by === void 0 ? "" : ` by ${parsed.by}`}
|
|
1541
|
+
`);
|
|
1542
|
+
write(io.stdout, `Path: ${formatRecordPath(paths, result.record)}
|
|
1543
|
+
`);
|
|
1544
|
+
}
|
|
1545
|
+
} catch (error) {
|
|
1546
|
+
writeError(error, io);
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
async function runDr(options, args, io) {
|
|
1550
|
+
const [subcommand, ...subcommandArgs] = args;
|
|
1551
|
+
if (subcommand === "retrieve") {
|
|
1552
|
+
await runDrRetrieve(options, subcommandArgs, io);
|
|
1553
|
+
return;
|
|
1554
|
+
}
|
|
1555
|
+
if (subcommand === "get") {
|
|
1556
|
+
await runDrGet(options, subcommandArgs, io);
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1559
|
+
if (subcommand === "list") {
|
|
1560
|
+
await runDrList(options, subcommandArgs, io);
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
if (subcommand === "enable") {
|
|
1564
|
+
await runDrSetEnabled(options, subcommandArgs, true, io);
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
if (subcommand === "disable") {
|
|
1568
|
+
await runDrSetEnabled(options, subcommandArgs, false, io);
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
if (subcommand === "retire") {
|
|
1572
|
+
await runDrRetire(options, subcommandArgs, io);
|
|
1573
|
+
return;
|
|
1574
|
+
}
|
|
1575
|
+
if (subcommand === "promote") {
|
|
1576
|
+
await runDrPromote(options, subcommandArgs, io);
|
|
1577
|
+
return;
|
|
1578
|
+
}
|
|
1579
|
+
if (subcommand === "delete") {
|
|
1580
|
+
await runDrDelete(options, subcommandArgs, io);
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1583
|
+
write(io.stderr, "Usage: codesteward dr (retrieve | get | list | enable | disable | retire | promote | delete)\n");
|
|
1584
|
+
io.exitCode = 64;
|
|
1585
|
+
}
|
|
1586
|
+
async function runDrRetrieve(options, args, io) {
|
|
1587
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
1588
|
+
write(io.stdout, retrieveHelp());
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1591
|
+
const parsed = parseRetrieveArgs(args, io);
|
|
1592
|
+
if (parsed === void 0) {
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
const paths = await requireStore(options.cwd, io);
|
|
1596
|
+
if (paths === void 0) {
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1599
|
+
const vocabulary = await readTagVocabulary(paths.tags);
|
|
1600
|
+
const knownTags = new Set(vocabulary.tags.map((tag) => tag.name));
|
|
1601
|
+
const unknownTags = parsed.tags.filter((tag) => !knownTags.has(tag));
|
|
1602
|
+
const unknownDomain = unknownQueryDomain(vocabulary.domains, parsed.domain);
|
|
1603
|
+
if (unknownDomain !== void 0 || unknownTags.length > 0) {
|
|
1604
|
+
if (unknownDomain !== void 0) {
|
|
1605
|
+
write(io.stderr, `Unknown domain "${unknownDomain}".
|
|
1606
|
+
`);
|
|
1607
|
+
}
|
|
1608
|
+
for (const tag of unknownTags) {
|
|
1609
|
+
write(io.stderr, `Unknown tag "${tag}".
|
|
1610
|
+
`);
|
|
1611
|
+
}
|
|
1612
|
+
io.exitCode = 1;
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
const records = await listDecisionRecords(paths, "accepted");
|
|
1616
|
+
const matches = records.filter((record) => recordMatches(record, parsed.tags, parsed.domain));
|
|
1617
|
+
if (options.quiet) {
|
|
1618
|
+
return;
|
|
1619
|
+
}
|
|
1620
|
+
renderRecords(matches, "medium", paths, io, retrieveRecordRenderOptions);
|
|
1621
|
+
}
|
|
1622
|
+
async function runDrGet(options, args, io) {
|
|
1623
|
+
const parsed = parseGetArgs(args, io);
|
|
1624
|
+
if (parsed === void 0) {
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
const paths = await requireStore(options.cwd, io);
|
|
1628
|
+
if (paths === void 0) {
|
|
1629
|
+
return;
|
|
1630
|
+
}
|
|
1631
|
+
const byId = await findDecisionRecordFilesById(paths, parsed.ids);
|
|
1632
|
+
for (const id of parsed.ids) {
|
|
1633
|
+
if (!byId.has(id)) {
|
|
1634
|
+
write(io.stderr, `No DR found with id "${id}".
|
|
1635
|
+
`);
|
|
1636
|
+
io.exitCode = 1;
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
if (options.quiet) {
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
for (const id of parsed.ids) {
|
|
1643
|
+
const record = byId.get(id);
|
|
1644
|
+
if (record === void 0) {
|
|
1645
|
+
continue;
|
|
1646
|
+
}
|
|
1647
|
+
write(io.stdout, await fs6.readFile(record, "utf8"));
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
async function findDecisionRecordFilesById(paths, ids) {
|
|
1651
|
+
const wanted = new Set(ids);
|
|
1652
|
+
const found = /* @__PURE__ */ new Map();
|
|
1653
|
+
for (const status of decisionRecordStatuses) {
|
|
1654
|
+
const directory = decisionRecordDirectory(paths, status);
|
|
1655
|
+
let entries;
|
|
1656
|
+
try {
|
|
1657
|
+
entries = await fs6.readdir(directory);
|
|
1658
|
+
} catch (error) {
|
|
1659
|
+
if (isNodeError4(error) && error.code === "ENOENT") {
|
|
1660
|
+
continue;
|
|
1661
|
+
}
|
|
1662
|
+
throw error;
|
|
1663
|
+
}
|
|
1664
|
+
for (const name of [...entries].sort()) {
|
|
1665
|
+
if (!name.toLowerCase().endsWith(".md")) {
|
|
1666
|
+
continue;
|
|
1667
|
+
}
|
|
1668
|
+
for (const id of wanted) {
|
|
1669
|
+
if (!found.has(id) && (name === `${id}.md` || name.startsWith(`${id}-`))) {
|
|
1670
|
+
found.set(id, path5.join(directory, name));
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
return found;
|
|
1676
|
+
}
|
|
1677
|
+
async function runDrList(options, args, io) {
|
|
1678
|
+
const parsed = parseListArgs(args, io);
|
|
1679
|
+
if (parsed === void 0) {
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1682
|
+
const paths = await requireStore(options.cwd, io);
|
|
1683
|
+
if (paths === void 0) {
|
|
1684
|
+
return;
|
|
1685
|
+
}
|
|
1686
|
+
const records = await listDecisionRecords(paths, parsed.status);
|
|
1687
|
+
const matches = parsed.tag === void 0 ? records : records.filter((record) => getRecordTags(record).includes(parsed.tag ?? ""));
|
|
1688
|
+
if (options.quiet) {
|
|
1689
|
+
return;
|
|
1690
|
+
}
|
|
1691
|
+
renderRecordList(matches, io);
|
|
1692
|
+
}
|
|
1693
|
+
async function runDrSetEnabled(options, args, enabled, io) {
|
|
1694
|
+
if (args.length !== 1) {
|
|
1695
|
+
write(io.stderr, `Usage: codesteward dr ${enabled ? "enable" : "disable"} <id>
|
|
1696
|
+
`);
|
|
1697
|
+
io.exitCode = 64;
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
const paths = await requireStore(options.cwd, io);
|
|
1701
|
+
if (paths === void 0) {
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
try {
|
|
1705
|
+
const result = await setDecisionRecordEnabled(paths, args[0], enabled);
|
|
1706
|
+
if (!options.quiet) {
|
|
1707
|
+
write(io.stdout, `${enabled ? "Enabled" : "Disabled"} ${getRecordId(result.record)}
|
|
1708
|
+
`);
|
|
1709
|
+
write(io.stdout, `Path: ${formatRecordPath(paths, result.record)}
|
|
1710
|
+
`);
|
|
1711
|
+
}
|
|
1712
|
+
} catch (error) {
|
|
1713
|
+
writeError(error, io);
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
async function runDrRetire(options, args, io) {
|
|
1717
|
+
const parsed = parseDrRetireArgs(args, io);
|
|
1718
|
+
if (parsed === void 0) {
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
const paths = await requireStore(options.cwd, io);
|
|
1722
|
+
if (paths === void 0) {
|
|
1723
|
+
return;
|
|
1724
|
+
}
|
|
1725
|
+
try {
|
|
1726
|
+
const result = await retireDecisionRecord(paths, parsed.id, parsed.by);
|
|
1727
|
+
if (!options.quiet) {
|
|
1728
|
+
write(io.stdout, `Retired ${parsed.id}${parsed.by === void 0 ? "" : ` by ${parsed.by}`}
|
|
1729
|
+
`);
|
|
1730
|
+
write(io.stdout, `Path: ${formatRecordPath(paths, result.record)}
|
|
1731
|
+
`);
|
|
1732
|
+
}
|
|
1733
|
+
} catch (error) {
|
|
1734
|
+
writeError(error, io);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
async function runDrPromote(options, args, io) {
|
|
1738
|
+
const parsed = parseDrPromoteArgs(args, io);
|
|
1739
|
+
if (parsed === void 0) {
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1742
|
+
const paths = await requireStore(options.cwd, io);
|
|
1743
|
+
if (paths === void 0) {
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
const from = parsed.from ?? await findHistoricalRecordStatus(paths, parsed.id);
|
|
1747
|
+
if (from === void 0) {
|
|
1748
|
+
write(io.stderr, `No rejected or retired DR found with id "${parsed.id}".
|
|
1749
|
+
`);
|
|
1750
|
+
io.exitCode = 1;
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1753
|
+
try {
|
|
1754
|
+
const result = await promoteDecisionRecord(paths, await readTagVocabulary(paths.tags), parsed.id, from, today());
|
|
1755
|
+
if (!options.quiet) {
|
|
1756
|
+
write(io.stdout, `Promoted ${parsed.id} as ${getRecordId(result.record)}
|
|
1757
|
+
`);
|
|
1758
|
+
write(io.stdout, `Path: ${formatRecordPath(paths, result.record)}
|
|
1759
|
+
`);
|
|
1760
|
+
}
|
|
1761
|
+
} catch (error) {
|
|
1762
|
+
writeError(error, io);
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
async function runDrDelete(options, args, io) {
|
|
1766
|
+
const parsed = parseDrDeleteArgs(args, io);
|
|
1767
|
+
if (parsed === void 0) {
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1770
|
+
const paths = await requireStore(options.cwd, io);
|
|
1771
|
+
if (paths === void 0) {
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
const from = parsed.from ?? await findHistoricalRecordStatus(paths, parsed.id);
|
|
1775
|
+
if (from === void 0) {
|
|
1776
|
+
write(io.stderr, `No rejected or retired DR found with id "${parsed.id}".
|
|
1777
|
+
`);
|
|
1778
|
+
io.exitCode = 1;
|
|
1779
|
+
return;
|
|
1780
|
+
}
|
|
1781
|
+
try {
|
|
1782
|
+
const result = await deleteDecisionRecord(paths, parsed.id, from);
|
|
1783
|
+
if (!options.quiet) {
|
|
1784
|
+
write(io.stdout, `Deleted ${result.id} ${result.title} (${result.status})
|
|
1785
|
+
`);
|
|
1786
|
+
write(io.stdout, `Removed: ${formatPath(paths, result.from)}
|
|
1787
|
+
`);
|
|
1788
|
+
}
|
|
1789
|
+
} catch (error) {
|
|
1790
|
+
writeError(error, io);
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
function renderValidationResults(results, io) {
|
|
1794
|
+
for (const result of results) {
|
|
1795
|
+
if (result.errors.length === 0 && result.warnings.length === 0) {
|
|
1796
|
+
continue;
|
|
1797
|
+
}
|
|
1798
|
+
write(io.stdout, `${result.path}
|
|
1799
|
+
`);
|
|
1800
|
+
for (const error of result.errors) {
|
|
1801
|
+
write(io.stdout, ` error: ${error}
|
|
1802
|
+
`);
|
|
1803
|
+
}
|
|
1804
|
+
for (const warning of result.warnings) {
|
|
1805
|
+
write(io.stdout, ` warning: ${warning}
|
|
1806
|
+
`);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
function renderStoreValidationResult(result, io) {
|
|
1811
|
+
for (const error of result.vocabularyErrors) {
|
|
1812
|
+
write(io.stdout, `Vocabulary error: ${error}
|
|
1813
|
+
`);
|
|
1814
|
+
}
|
|
1815
|
+
renderValidationResults(result.validationResults, io);
|
|
1816
|
+
}
|
|
1817
|
+
function parseRetrieveArgs(args, io) {
|
|
1818
|
+
const tags = [];
|
|
1819
|
+
let domain;
|
|
1820
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
1821
|
+
const arg = args[index];
|
|
1822
|
+
if (arg === "--tag") {
|
|
1823
|
+
const value = args[index + 1];
|
|
1824
|
+
if (value === void 0) {
|
|
1825
|
+
return usageError(retrieveUsage(), io);
|
|
1826
|
+
}
|
|
1827
|
+
tags.push(value);
|
|
1828
|
+
index += 1;
|
|
1829
|
+
continue;
|
|
1830
|
+
}
|
|
1831
|
+
if (arg === "--domain") {
|
|
1832
|
+
const value = args[index + 1];
|
|
1833
|
+
if (value === void 0 || !isValidRecordDomain(value)) {
|
|
1834
|
+
return usageError(retrieveUsage(), io);
|
|
1835
|
+
}
|
|
1836
|
+
domain = value;
|
|
1837
|
+
index += 1;
|
|
1838
|
+
continue;
|
|
1839
|
+
}
|
|
1840
|
+
return usageError(retrieveUsage(), io);
|
|
1841
|
+
}
|
|
1842
|
+
return { tags, domain };
|
|
1843
|
+
}
|
|
1844
|
+
function parseGetArgs(args, io) {
|
|
1845
|
+
const ids = [];
|
|
1846
|
+
for (const arg of args) {
|
|
1847
|
+
if (arg.startsWith("-")) {
|
|
1848
|
+
return usageError("Usage: codesteward dr get <id> [<id>]\n", io);
|
|
1849
|
+
}
|
|
1850
|
+
ids.push(arg);
|
|
1851
|
+
}
|
|
1852
|
+
if (ids.length === 0) {
|
|
1853
|
+
return usageError("Usage: codesteward dr get <id> [<id>]\n", io);
|
|
1854
|
+
}
|
|
1855
|
+
return { ids };
|
|
1856
|
+
}
|
|
1857
|
+
function parseListArgs(args, io) {
|
|
1858
|
+
let status;
|
|
1859
|
+
let tag;
|
|
1860
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
1861
|
+
const arg = args[index];
|
|
1862
|
+
if (arg === "--status") {
|
|
1863
|
+
const value = args[index + 1];
|
|
1864
|
+
if (!isDecisionRecordStatus(value)) {
|
|
1865
|
+
return usageError("Usage: codesteward dr list [--status candidate|accepted|rejected|retired] [--tag <tag>]\n", io);
|
|
1866
|
+
}
|
|
1867
|
+
status = value;
|
|
1868
|
+
index += 1;
|
|
1869
|
+
continue;
|
|
1870
|
+
}
|
|
1871
|
+
if (arg === "--tag") {
|
|
1872
|
+
const value = args[index + 1];
|
|
1873
|
+
if (value === void 0) {
|
|
1874
|
+
return usageError("Usage: codesteward dr list [--status candidate|accepted|rejected|retired] [--tag <tag>]\n", io);
|
|
1875
|
+
}
|
|
1876
|
+
tag = value;
|
|
1877
|
+
index += 1;
|
|
1878
|
+
continue;
|
|
1879
|
+
}
|
|
1880
|
+
return usageError("Usage: codesteward dr list [--status candidate|accepted|rejected|retired] [--tag <tag>]\n", io);
|
|
1881
|
+
}
|
|
1882
|
+
return { status, tag };
|
|
1883
|
+
}
|
|
1884
|
+
function parseCandidateCreateArgs(args, io) {
|
|
1885
|
+
let title;
|
|
1886
|
+
let domain = "all";
|
|
1887
|
+
let domainSetBy;
|
|
1888
|
+
let decision;
|
|
1889
|
+
let appendix;
|
|
1890
|
+
const affectedFiles = [];
|
|
1891
|
+
const tags = [];
|
|
1892
|
+
const proposedTagDescriptions = {};
|
|
1893
|
+
let proposedDomainDescription;
|
|
1894
|
+
const references = [];
|
|
1895
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
1896
|
+
const arg = args[index];
|
|
1897
|
+
const value = args[index + 1];
|
|
1898
|
+
if (arg === "--title") {
|
|
1899
|
+
if (value === void 0) {
|
|
1900
|
+
return usageError(candidateCreateUsage(), io);
|
|
1901
|
+
}
|
|
1902
|
+
title = value;
|
|
1903
|
+
index += 1;
|
|
1904
|
+
continue;
|
|
1905
|
+
}
|
|
1906
|
+
if (arg === "--domain") {
|
|
1907
|
+
if (value === void 0 || !isValidRecordDomain(value) || domainSetBy !== void 0) {
|
|
1908
|
+
return usageError(candidateCreateUsage(), io);
|
|
1909
|
+
}
|
|
1910
|
+
domain = value;
|
|
1911
|
+
domainSetBy = "domain";
|
|
1912
|
+
index += 1;
|
|
1913
|
+
continue;
|
|
1914
|
+
}
|
|
1915
|
+
if (arg === "--proposed-domain") {
|
|
1916
|
+
const description = args[index + 2];
|
|
1917
|
+
if (value === void 0 || description === void 0 || !isValidRecordDomain(value) || domainSetBy !== void 0) {
|
|
1918
|
+
return usageError(candidateCreateUsage(), io);
|
|
1919
|
+
}
|
|
1920
|
+
domain = value;
|
|
1921
|
+
domainSetBy = "proposed-domain";
|
|
1922
|
+
proposedDomainDescription = description;
|
|
1923
|
+
index += 2;
|
|
1924
|
+
continue;
|
|
1925
|
+
}
|
|
1926
|
+
if (arg === "--decision") {
|
|
1927
|
+
if (value === void 0) {
|
|
1928
|
+
return usageError(candidateCreateUsage(), io);
|
|
1929
|
+
}
|
|
1930
|
+
decision = value;
|
|
1931
|
+
index += 1;
|
|
1932
|
+
continue;
|
|
1933
|
+
}
|
|
1934
|
+
if (arg === "--appendix") {
|
|
1935
|
+
if (value === void 0) {
|
|
1936
|
+
return usageError(candidateCreateUsage(), io);
|
|
1937
|
+
}
|
|
1938
|
+
appendix = value;
|
|
1939
|
+
index += 1;
|
|
1940
|
+
continue;
|
|
1941
|
+
}
|
|
1942
|
+
if (arg === "--affected") {
|
|
1943
|
+
if (value === void 0) {
|
|
1944
|
+
return usageError(candidateCreateUsage(), io);
|
|
1945
|
+
}
|
|
1946
|
+
affectedFiles.push(value);
|
|
1947
|
+
index += 1;
|
|
1948
|
+
continue;
|
|
1949
|
+
}
|
|
1950
|
+
if (arg === "--tag") {
|
|
1951
|
+
if (value === void 0) {
|
|
1952
|
+
return usageError(candidateCreateUsage(), io);
|
|
1953
|
+
}
|
|
1954
|
+
tags.push(value);
|
|
1955
|
+
index += 1;
|
|
1956
|
+
continue;
|
|
1957
|
+
}
|
|
1958
|
+
if (arg === "--proposed-tag") {
|
|
1959
|
+
const description = args[index + 2];
|
|
1960
|
+
if (value === void 0 || description === void 0) {
|
|
1961
|
+
return usageError(candidateCreateUsage(), io);
|
|
1962
|
+
}
|
|
1963
|
+
tags.push(value);
|
|
1964
|
+
proposedTagDescriptions[value] = description;
|
|
1965
|
+
index += 2;
|
|
1966
|
+
continue;
|
|
1967
|
+
}
|
|
1968
|
+
if (arg === "--ref") {
|
|
1969
|
+
if (value === void 0) {
|
|
1970
|
+
return usageError(candidateCreateUsage(), io);
|
|
1971
|
+
}
|
|
1972
|
+
references.push(value);
|
|
1973
|
+
index += 1;
|
|
1974
|
+
continue;
|
|
1975
|
+
}
|
|
1976
|
+
return usageError(candidateCreateUsage(), io);
|
|
1977
|
+
}
|
|
1978
|
+
if (title === void 0 || decision === void 0) {
|
|
1979
|
+
return usageError(candidateCreateUsage(), io);
|
|
1980
|
+
}
|
|
1981
|
+
return {
|
|
1982
|
+
title,
|
|
1983
|
+
domain,
|
|
1984
|
+
decision,
|
|
1985
|
+
appendix: appendix?.trim() || void 0,
|
|
1986
|
+
affectedFiles,
|
|
1987
|
+
tags,
|
|
1988
|
+
proposedTagDescriptions,
|
|
1989
|
+
proposedDomainDescription,
|
|
1990
|
+
references
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
function parseCandidateListArgs(args, io) {
|
|
1994
|
+
let status = "candidate";
|
|
1995
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
1996
|
+
const arg = args[index];
|
|
1997
|
+
if (arg === "--status") {
|
|
1998
|
+
const value = args[index + 1];
|
|
1999
|
+
if (!isDecisionRecordStatus(value)) {
|
|
2000
|
+
return usageError("Usage: codesteward candidate list [--status candidate|rejected|retired]\n", io);
|
|
2001
|
+
}
|
|
2002
|
+
status = value;
|
|
2003
|
+
index += 1;
|
|
2004
|
+
continue;
|
|
2005
|
+
}
|
|
2006
|
+
if (arg === "--all") {
|
|
2007
|
+
status = void 0;
|
|
2008
|
+
continue;
|
|
2009
|
+
}
|
|
2010
|
+
return usageError("Usage: codesteward candidate list [--status candidate|rejected|retired]\n", io);
|
|
2011
|
+
}
|
|
2012
|
+
return { status };
|
|
2013
|
+
}
|
|
2014
|
+
function parseCandidateRejectArgs(args, io) {
|
|
2015
|
+
const [id, ...rest] = args;
|
|
2016
|
+
let reason;
|
|
2017
|
+
if (id === void 0) {
|
|
2018
|
+
return usageError("Usage: codesteward candidate reject <id> [--reason <text>]\n", io);
|
|
2019
|
+
}
|
|
2020
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
2021
|
+
const arg = rest[index];
|
|
2022
|
+
if (arg !== "--reason") {
|
|
2023
|
+
return usageError("Usage: codesteward candidate reject <id> [--reason <text>]\n", io);
|
|
2024
|
+
}
|
|
2025
|
+
const value = rest[index + 1];
|
|
2026
|
+
if (value === void 0) {
|
|
2027
|
+
return usageError("Usage: codesteward candidate reject <id> [--reason <text>]\n", io);
|
|
2028
|
+
}
|
|
2029
|
+
reason = value;
|
|
2030
|
+
index += 1;
|
|
2031
|
+
}
|
|
2032
|
+
return { id, reason };
|
|
2033
|
+
}
|
|
2034
|
+
function parseCandidateRetireArgs(args, io) {
|
|
2035
|
+
return parseRetireArgs(args, "candidate retire", io);
|
|
2036
|
+
}
|
|
2037
|
+
function parseDrRetireArgs(args, io) {
|
|
2038
|
+
return parseRetireArgs(args, "dr retire", io);
|
|
2039
|
+
}
|
|
2040
|
+
function parseRetireArgs(args, command, io) {
|
|
2041
|
+
const [id, ...rest] = args;
|
|
2042
|
+
let by;
|
|
2043
|
+
const usage2 = `Usage: codesteward ${command} <id> [--by <id>]
|
|
2044
|
+
`;
|
|
2045
|
+
if (id === void 0) {
|
|
2046
|
+
return usageError(usage2, io);
|
|
2047
|
+
}
|
|
2048
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
2049
|
+
const arg = rest[index];
|
|
2050
|
+
if (arg !== "--by") {
|
|
2051
|
+
return usageError(usage2, io);
|
|
2052
|
+
}
|
|
2053
|
+
const value = rest[index + 1];
|
|
2054
|
+
if (value === void 0) {
|
|
2055
|
+
return usageError(usage2, io);
|
|
2056
|
+
}
|
|
2057
|
+
by = value;
|
|
2058
|
+
index += 1;
|
|
2059
|
+
}
|
|
2060
|
+
return { id, by };
|
|
2061
|
+
}
|
|
2062
|
+
function parseDrPromoteArgs(args, io) {
|
|
2063
|
+
return parseHistoricalDrArgs(args, "promote", io);
|
|
2064
|
+
}
|
|
2065
|
+
function parseDrDeleteArgs(args, io) {
|
|
2066
|
+
return parseHistoricalDrArgs(args, "delete", io);
|
|
2067
|
+
}
|
|
2068
|
+
function parseHistoricalDrArgs(args, command, io) {
|
|
2069
|
+
const [id, ...rest] = args;
|
|
2070
|
+
let from;
|
|
2071
|
+
const usage2 = `Usage: codesteward dr ${command} <id> [--from rejected|retired]
|
|
2072
|
+
`;
|
|
2073
|
+
if (id === void 0) {
|
|
2074
|
+
return usageError(usage2, io);
|
|
2075
|
+
}
|
|
2076
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
2077
|
+
const arg = rest[index];
|
|
2078
|
+
if (arg !== "--from") {
|
|
2079
|
+
return usageError(usage2, io);
|
|
2080
|
+
}
|
|
2081
|
+
const value = rest[index + 1];
|
|
2082
|
+
if (value !== "rejected" && value !== "retired") {
|
|
2083
|
+
return usageError(usage2, io);
|
|
2084
|
+
}
|
|
2085
|
+
from = value;
|
|
2086
|
+
index += 1;
|
|
2087
|
+
}
|
|
2088
|
+
return { id, from };
|
|
2089
|
+
}
|
|
2090
|
+
function parseBootstrapArgs(args, io) {
|
|
2091
|
+
let provider;
|
|
2092
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
2093
|
+
const arg = args[index];
|
|
2094
|
+
if (arg !== "--provider") {
|
|
2095
|
+
return usageError(bootstrapUsage(), io);
|
|
2096
|
+
}
|
|
2097
|
+
const value = args[index + 1];
|
|
2098
|
+
if (value !== "claude" && value !== "codex") {
|
|
2099
|
+
return usageError(bootstrapUsage(), io);
|
|
2100
|
+
}
|
|
2101
|
+
provider = value;
|
|
2102
|
+
index += 1;
|
|
2103
|
+
}
|
|
2104
|
+
if (provider === void 0) {
|
|
2105
|
+
return usageError(bootstrapUsage(), io);
|
|
2106
|
+
}
|
|
2107
|
+
return { provider };
|
|
2108
|
+
}
|
|
2109
|
+
function bootstrapUsage() {
|
|
2110
|
+
return "Usage: codesteward bootstrap --provider claude|codex\n";
|
|
2111
|
+
}
|
|
2112
|
+
function bootstrapCommand(provider, root, prompt) {
|
|
2113
|
+
if (provider === "claude") {
|
|
2114
|
+
return {
|
|
2115
|
+
file: "claude",
|
|
2116
|
+
args: [
|
|
2117
|
+
"--print",
|
|
2118
|
+
"--permission-mode",
|
|
2119
|
+
"acceptEdits",
|
|
2120
|
+
"--allowedTools",
|
|
2121
|
+
[
|
|
2122
|
+
"Read",
|
|
2123
|
+
"Glob",
|
|
2124
|
+
"Grep",
|
|
2125
|
+
"Bash(codesteward --cwd * candidate create *)",
|
|
2126
|
+
"Bash(codesteward --cwd * tags)",
|
|
2127
|
+
"Bash(codesteward --cwd * dr list *)"
|
|
2128
|
+
].join(","),
|
|
2129
|
+
prompt
|
|
2130
|
+
]
|
|
2131
|
+
};
|
|
2132
|
+
}
|
|
2133
|
+
return {
|
|
2134
|
+
file: "codex",
|
|
2135
|
+
args: [
|
|
2136
|
+
"exec",
|
|
2137
|
+
"--cd",
|
|
2138
|
+
root,
|
|
2139
|
+
"--full-auto",
|
|
2140
|
+
"--skip-git-repo-check",
|
|
2141
|
+
prompt
|
|
2142
|
+
]
|
|
2143
|
+
};
|
|
2144
|
+
}
|
|
2145
|
+
async function runBootstrapCommand(command, cwd, io) {
|
|
2146
|
+
await new Promise((resolve3, reject) => {
|
|
2147
|
+
const child = (0, import_node_child_process.spawn)(command.file, command.args, {
|
|
2148
|
+
cwd,
|
|
2149
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2150
|
+
});
|
|
2151
|
+
let settled = false;
|
|
2152
|
+
child.stdout.on("data", (chunk) => {
|
|
2153
|
+
io?.stdout.write(chunk);
|
|
2154
|
+
});
|
|
2155
|
+
child.stderr.on("data", (chunk) => {
|
|
2156
|
+
io?.stderr.write(chunk);
|
|
2157
|
+
});
|
|
2158
|
+
child.on("error", (error) => {
|
|
2159
|
+
if (settled) {
|
|
2160
|
+
return;
|
|
2161
|
+
}
|
|
2162
|
+
settled = true;
|
|
2163
|
+
reject(error);
|
|
2164
|
+
});
|
|
2165
|
+
child.on("close", (code, signal) => {
|
|
2166
|
+
if (settled) {
|
|
2167
|
+
return;
|
|
2168
|
+
}
|
|
2169
|
+
settled = true;
|
|
2170
|
+
if (code === 0) {
|
|
2171
|
+
resolve3();
|
|
2172
|
+
return;
|
|
2173
|
+
}
|
|
2174
|
+
if (signal !== null) {
|
|
2175
|
+
reject(new Error(`Command failed with signal ${signal}: ${formatBootstrapCommand(command)}`));
|
|
2176
|
+
return;
|
|
2177
|
+
}
|
|
2178
|
+
reject(new Error(`Command failed with exit code ${code ?? "unknown"}: ${formatBootstrapCommand(command)}`));
|
|
2179
|
+
});
|
|
2180
|
+
});
|
|
2181
|
+
}
|
|
2182
|
+
function formatBootstrapCommand(command) {
|
|
2183
|
+
const args = command.args.map((arg, index) => {
|
|
2184
|
+
const isPrompt = index === command.args.length - 1;
|
|
2185
|
+
return isPrompt ? "<prompt>" : shellQuote(arg);
|
|
2186
|
+
});
|
|
2187
|
+
return [shellQuote(command.file), ...args].join(" ");
|
|
2188
|
+
}
|
|
2189
|
+
function bootstrapPrompt(paths) {
|
|
2190
|
+
const root = paths.root;
|
|
2191
|
+
return [
|
|
2192
|
+
"You are running a CodeSteward bootstrap.",
|
|
2193
|
+
"",
|
|
2194
|
+
`Project root: ${root}`,
|
|
2195
|
+
"",
|
|
2196
|
+
"Goal:",
|
|
2197
|
+
"- Inspect existing project instructions, markdown documentation, and representative source code.",
|
|
2198
|
+
"- Find consequential project-specific decisions that should become CodeSteward Decision Record candidates.",
|
|
2199
|
+
"- Create candidates only by running the public CodeSteward CLI. Do not write .codesteward files directly.",
|
|
2200
|
+
"",
|
|
2201
|
+
"Important candidate creation contract:",
|
|
2202
|
+
`- Use this command shape: codesteward --cwd ${shellQuote(root)} candidate create --title "<title>" --domain "<domain>" --decision "<terse guidance>"`,
|
|
2203
|
+
'- Use --proposed-domain <domain> "<description>" instead of --domain when the domain is not already listed in .codesteward/tags.md.',
|
|
2204
|
+
"- Add --tag <known-tag> when a known tag applies.",
|
|
2205
|
+
'- Use --proposed-tag <tag> "<description>" when a new tag is truly useful.',
|
|
2206
|
+
'- Add --appendix "<human-facing context>" only when explanatory background helps reviewers; do not put governing guidance there.',
|
|
2207
|
+
"- Add --affected <path> and --ref <path-or-symbol> when useful.",
|
|
2208
|
+
"- Use existing domains and tags from .codesteward/tags.md when possible. Proposed vocabulary is for truly useful missing terms.",
|
|
2209
|
+
"- Every DR candidate must go through `codesteward candidate create`; do not create markdown files manually.",
|
|
2210
|
+
"",
|
|
2211
|
+
"What to inspect:",
|
|
2212
|
+
"- AGENTS.md, AGENT.md, CLAUDE.md, .agents/**, .claude/**.",
|
|
2213
|
+
"- README files, architecture/design/ADR docs, implementation plans, and other meaningful markdown.",
|
|
2214
|
+
"- Source tree structure and representative implementation files that reveal project conventions.",
|
|
2215
|
+
"- Existing accepted and candidate DRs, so you do not duplicate them.",
|
|
2216
|
+
"",
|
|
2217
|
+
"What to avoid:",
|
|
2218
|
+
"- Do not create candidates for generic best practices, obvious framework behavior, style preferences with no architectural consequence, or speculation.",
|
|
2219
|
+
"- Do not edit application source, docs, tags, or accepted DRs.",
|
|
2220
|
+
"- Prefer fewer, high-signal candidates.",
|
|
2221
|
+
"",
|
|
2222
|
+
"After creating candidates, summarize what you created. If no DR-worthy decisions exist, say that and create nothing."
|
|
2223
|
+
].join("\n");
|
|
2224
|
+
}
|
|
2225
|
+
function shellQuote(value) {
|
|
2226
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
2227
|
+
}
|
|
2228
|
+
function candidateCreateUsage() {
|
|
2229
|
+
return "Usage: codesteward candidate create --title <title> [--domain <domain> | --proposed-domain <domain> <description>] --decision <text> [--appendix <text>] [--tag <tag>] [--proposed-tag <tag> <description>] [--affected <path>] [--ref <ref>]\n";
|
|
2230
|
+
}
|
|
2231
|
+
function retrieveUsage() {
|
|
2232
|
+
return "Usage: codesteward dr retrieve [--domain <domain>] [--tag <tag>...]\n";
|
|
2233
|
+
}
|
|
2234
|
+
function retrieveHelp() {
|
|
2235
|
+
return `Usage: codesteward dr retrieve [--domain <domain>] [--tag <tag>...]
|
|
2236
|
+
|
|
2237
|
+
Retrieve visible accepted Decision Records.
|
|
2238
|
+
|
|
2239
|
+
Options:
|
|
2240
|
+
--domain <domain> Filter by domain. Matches the exact domain, its ancestors, and its descendants. Omit to match every domain.
|
|
2241
|
+
--tag <tag> Filter by tag. Repeat for multiple tags. A DR matches when any provided tag is on it; DRs with no tags always match.
|
|
2242
|
+
`;
|
|
2243
|
+
}
|
|
2244
|
+
function renderRecords(records, detail, paths, io, options = defaultRecordRenderOptions) {
|
|
2245
|
+
if (records.length === 0) {
|
|
2246
|
+
write(io.stdout, "No matching DRs.\n");
|
|
2247
|
+
return;
|
|
2248
|
+
}
|
|
2249
|
+
for (const [index, record] of records.entries()) {
|
|
2250
|
+
if (index > 0) {
|
|
2251
|
+
write(io.stdout, "\n");
|
|
2252
|
+
}
|
|
2253
|
+
renderRecord(record, detail, paths, io, options);
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
function renderRecord(record, detail, paths, io, options = defaultRecordRenderOptions) {
|
|
2257
|
+
if (detail === "full") {
|
|
2258
|
+
write(io.stdout, record.markdown.trimEnd());
|
|
2259
|
+
write(io.stdout, "\n");
|
|
2260
|
+
return;
|
|
2261
|
+
}
|
|
2262
|
+
write(io.stdout, `${getRecordId(record)} ${getRecordTitle(record)}
|
|
2263
|
+
`);
|
|
2264
|
+
if (options.showStatus) {
|
|
2265
|
+
write(io.stdout, `Status: ${getRecordStatus(record)}
|
|
2266
|
+
`);
|
|
2267
|
+
}
|
|
2268
|
+
if (options.showEnabled && !getRecordEnabled(record)) {
|
|
2269
|
+
write(io.stdout, "Enabled: false\n");
|
|
2270
|
+
}
|
|
2271
|
+
write(io.stdout, `Domain: ${getRecordDomain(record)}
|
|
2272
|
+
`);
|
|
2273
|
+
write(io.stdout, `Tags: ${formatRecordTags(record)}
|
|
2274
|
+
`);
|
|
2275
|
+
if (detail === "medium") {
|
|
2276
|
+
write(io.stdout, `Decision:
|
|
2277
|
+
${getRecordDecision(record)}
|
|
2278
|
+
`);
|
|
2279
|
+
renderOptionalSection(record, "Applies When", "Applies when", io);
|
|
2280
|
+
renderOptionalSection(record, "Does Not Apply When", "Does not apply when", io);
|
|
2281
|
+
const references = getRecordReferences(record);
|
|
2282
|
+
if (references.length > 0) {
|
|
2283
|
+
write(io.stdout, "References:\n");
|
|
2284
|
+
for (const reference of references) {
|
|
2285
|
+
write(io.stdout, `- ${reference}
|
|
2286
|
+
`);
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
if (options.showPath) {
|
|
2291
|
+
write(io.stdout, `Path: ${formatRecordPath(paths, record)}
|
|
2292
|
+
`);
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
function renderOptionalSection(record, sectionTitle, outputTitle, io) {
|
|
2296
|
+
const section = getRecordSection(record, sectionTitle);
|
|
2297
|
+
if (section === void 0 || section.length === 0) {
|
|
2298
|
+
return;
|
|
2299
|
+
}
|
|
2300
|
+
write(io.stdout, `${outputTitle}:
|
|
2301
|
+
${section}
|
|
2302
|
+
`);
|
|
2303
|
+
}
|
|
2304
|
+
function recordMatches(record, tags, domain) {
|
|
2305
|
+
return getRecordEnabled(record) && recordMatchesTags(record, tags) && recordMatchesDomain(record, domain);
|
|
2306
|
+
}
|
|
2307
|
+
function recordMatchesTags(record, tags) {
|
|
2308
|
+
if (tags.length === 0) {
|
|
2309
|
+
return true;
|
|
2310
|
+
}
|
|
2311
|
+
const recordTagsList = getRecordTags(record);
|
|
2312
|
+
if (recordTagsList.length === 0) {
|
|
2313
|
+
return true;
|
|
2314
|
+
}
|
|
2315
|
+
const recordTags = new Set(recordTagsList);
|
|
2316
|
+
return tags.some((tag) => recordTags.has(tag));
|
|
2317
|
+
}
|
|
2318
|
+
function recordMatchesDomain(record, domain) {
|
|
2319
|
+
if (domain === void 0 || domain === "all") {
|
|
2320
|
+
return true;
|
|
2321
|
+
}
|
|
2322
|
+
const recordDomain = getRecordDomain(record);
|
|
2323
|
+
if (recordDomain === "all") {
|
|
2324
|
+
return true;
|
|
2325
|
+
}
|
|
2326
|
+
return recordDomain === domain || recordDomain.startsWith(`${domain}.`) || domain.startsWith(`${recordDomain}.`);
|
|
2327
|
+
}
|
|
2328
|
+
function formatRecordTags(record) {
|
|
2329
|
+
const tags = getRecordTags(record);
|
|
2330
|
+
return tags.length === 0 ? "(wildcard)" : tags.join(", ");
|
|
2331
|
+
}
|
|
2332
|
+
function formatRecordListItem(record) {
|
|
2333
|
+
return `${getRecordId(record)} ${getRecordTitle(record)} Domain: ${getRecordDomain(record)} Tags: ${formatRecordTags(record)}`;
|
|
2334
|
+
}
|
|
2335
|
+
function renderRecordList(records, io) {
|
|
2336
|
+
const groups = [
|
|
2337
|
+
{ heading: "Rejected", records: records.filter((record) => getRecordStatus(record) === "rejected") },
|
|
2338
|
+
{ heading: "Candidate", records: records.filter((record) => getRecordStatus(record) === "candidate") },
|
|
2339
|
+
{ heading: "Retired", records: records.filter((record) => getRecordStatus(record) === "retired") },
|
|
2340
|
+
{ heading: "Active", records: records.filter((record) => getRecordStatus(record) === "accepted" && getRecordEnabled(record)) },
|
|
2341
|
+
{ heading: "Active(hidden)", records: records.filter((record) => getRecordStatus(record) === "accepted" && !getRecordEnabled(record)) }
|
|
2342
|
+
];
|
|
2343
|
+
let renderedAnyGroup = false;
|
|
2344
|
+
for (const group of groups) {
|
|
2345
|
+
if (group.records.length === 0) {
|
|
2346
|
+
continue;
|
|
2347
|
+
}
|
|
2348
|
+
if (renderedAnyGroup) {
|
|
2349
|
+
write(io.stdout, "\n");
|
|
2350
|
+
}
|
|
2351
|
+
write(io.stdout, `${group.heading}:
|
|
2352
|
+
`);
|
|
2353
|
+
for (const record of group.records) {
|
|
2354
|
+
write(io.stdout, `${formatRecordListItem(record)}
|
|
2355
|
+
`);
|
|
2356
|
+
}
|
|
2357
|
+
renderedAnyGroup = true;
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
async function findHistoricalRecordStatus(paths, id) {
|
|
2361
|
+
for (const status of ["rejected", "retired"]) {
|
|
2362
|
+
const records = await listDecisionRecords(paths, status);
|
|
2363
|
+
if (records.some((record) => getRecordId(record) === id)) {
|
|
2364
|
+
return status;
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
return void 0;
|
|
2368
|
+
}
|
|
2369
|
+
function formatRecordPath(paths, record) {
|
|
2370
|
+
return formatPath(paths, record.filePath);
|
|
2371
|
+
}
|
|
2372
|
+
function formatPath(paths, targetPath) {
|
|
2373
|
+
const relativePath = path5.relative(paths.root, targetPath);
|
|
2374
|
+
return relativePath.split(path5.sep).join("/");
|
|
2375
|
+
}
|
|
2376
|
+
function usageError(message, io) {
|
|
2377
|
+
write(io.stderr, message);
|
|
2378
|
+
io.exitCode = 64;
|
|
2379
|
+
return void 0;
|
|
2380
|
+
}
|
|
2381
|
+
function isDecisionRecordStatus(value) {
|
|
2382
|
+
return value === "candidate" || value === "accepted" || value === "rejected" || value === "retired";
|
|
2383
|
+
}
|
|
2384
|
+
function today() {
|
|
2385
|
+
return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
2386
|
+
}
|
|
2387
|
+
function defaultAuthor() {
|
|
2388
|
+
return process.env.USER ?? process.env.USERNAME ?? "agent";
|
|
2389
|
+
}
|
|
2390
|
+
function writeError(error, io) {
|
|
2391
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2392
|
+
write(io.stderr, `${message}
|
|
2393
|
+
`);
|
|
2394
|
+
io.exitCode = 1;
|
|
2395
|
+
}
|
|
2396
|
+
async function runStatus(options, args, io) {
|
|
2397
|
+
if (args.length > 0) {
|
|
2398
|
+
write(io.stderr, `Unexpected arguments for status: ${args.join(" ")}
|
|
2399
|
+
`);
|
|
2400
|
+
io.exitCode = 64;
|
|
2401
|
+
return;
|
|
2402
|
+
}
|
|
2403
|
+
const paths = await requireStore(options.cwd, io);
|
|
2404
|
+
if (paths === void 0) {
|
|
2405
|
+
return;
|
|
2406
|
+
}
|
|
2407
|
+
const vocabulary = await readTagVocabulary(paths.tags);
|
|
2408
|
+
const validation = await validateStore(paths);
|
|
2409
|
+
const errorCount = validationErrorCount(validation);
|
|
2410
|
+
const warningCount = validationWarningCount(validation);
|
|
2411
|
+
const counts = await Promise.all(
|
|
2412
|
+
decisionRecordStatuses.map(async (status) => ({
|
|
2413
|
+
status,
|
|
2414
|
+
count: await countDecisionRecords(paths, status)
|
|
2415
|
+
}))
|
|
2416
|
+
);
|
|
2417
|
+
if (options.quiet) {
|
|
2418
|
+
if (errorCount > 0) {
|
|
2419
|
+
io.exitCode = 1;
|
|
2420
|
+
}
|
|
2421
|
+
return;
|
|
2422
|
+
}
|
|
2423
|
+
write(io.stdout, `CodeSteward store: ${paths.store}
|
|
2424
|
+
`);
|
|
2425
|
+
write(io.stdout, `Domains: ${vocabulary.domains.length}
|
|
2426
|
+
`);
|
|
2427
|
+
write(io.stdout, `Tags: ${vocabulary.tags.length}
|
|
2428
|
+
`);
|
|
2429
|
+
for (const item of counts) {
|
|
2430
|
+
write(io.stdout, `${formatStatusLabel(item.status)} DRs: ${item.count}
|
|
2431
|
+
`);
|
|
2432
|
+
}
|
|
2433
|
+
write(io.stdout, "\n");
|
|
2434
|
+
renderStoreValidationResult(validation, io);
|
|
2435
|
+
write(io.stdout, `Validation: ${errorCount} error${errorCount === 1 ? "" : "s"}, ${warningCount} warning${warningCount === 1 ? "" : "s"}.
|
|
2436
|
+
`);
|
|
2437
|
+
if (errorCount > 0) {
|
|
2438
|
+
io.exitCode = 1;
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
async function runUpdate(options, args, io) {
|
|
2442
|
+
const parsed = parseUpdateArgs(args, io);
|
|
2443
|
+
if (parsed === void 0) {
|
|
2444
|
+
return;
|
|
2445
|
+
}
|
|
2446
|
+
const paths = parsed.root === void 0 ? await requireStore(options.cwd, io) : await requireStoreAtRoot(parsed.root, io);
|
|
2447
|
+
if (paths === void 0) {
|
|
2448
|
+
return;
|
|
2449
|
+
}
|
|
2450
|
+
try {
|
|
2451
|
+
const result = await updateRuntimeAssets(paths.root, parsed);
|
|
2452
|
+
if (options.quiet) {
|
|
2453
|
+
return;
|
|
2454
|
+
}
|
|
2455
|
+
write(io.stdout, `Updated CodeSteward skill files at ${result.paths.root}
|
|
2456
|
+
`);
|
|
2457
|
+
renderPathGroup("Created", result.created, io);
|
|
2458
|
+
renderPathGroup("Already current", result.existing, io);
|
|
2459
|
+
renderPathGroup("Updated", result.updated, io);
|
|
2460
|
+
} catch (error) {
|
|
2461
|
+
writeError(error, io);
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
function parseUpdateArgs(args, io) {
|
|
2465
|
+
let root;
|
|
2466
|
+
let claude = false;
|
|
2467
|
+
let codex = false;
|
|
2468
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
2469
|
+
const arg = args[index];
|
|
2470
|
+
if (arg === "--root") {
|
|
2471
|
+
const value = args[index + 1];
|
|
2472
|
+
if (value === void 0) {
|
|
2473
|
+
return updateUsageError(io);
|
|
2474
|
+
}
|
|
2475
|
+
root = value;
|
|
2476
|
+
index += 1;
|
|
2477
|
+
continue;
|
|
2478
|
+
}
|
|
2479
|
+
if (arg === "--claude") {
|
|
2480
|
+
claude = true;
|
|
2481
|
+
continue;
|
|
2482
|
+
}
|
|
2483
|
+
if (arg === "--codex") {
|
|
2484
|
+
codex = true;
|
|
2485
|
+
continue;
|
|
2486
|
+
}
|
|
2487
|
+
return updateUsageError(io);
|
|
2488
|
+
}
|
|
2489
|
+
if (!claude && !codex) {
|
|
2490
|
+
return updateUsageError(io);
|
|
2491
|
+
}
|
|
2492
|
+
return { root, claude, codex };
|
|
2493
|
+
}
|
|
2494
|
+
function updateUsageError(io) {
|
|
2495
|
+
write(io.stderr, "Usage: codesteward update [--root <path>] --claude|--codex\n");
|
|
2496
|
+
io.exitCode = 64;
|
|
2497
|
+
return void 0;
|
|
2498
|
+
}
|
|
2499
|
+
function parseArguments(argv, defaultCwd) {
|
|
2500
|
+
const command = [];
|
|
2501
|
+
let cwd = defaultCwd;
|
|
2502
|
+
let quiet = false;
|
|
2503
|
+
let noSessionLog = false;
|
|
2504
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
2505
|
+
const arg = argv[index];
|
|
2506
|
+
if (command.length > 0) {
|
|
2507
|
+
command.push(arg);
|
|
2508
|
+
continue;
|
|
2509
|
+
}
|
|
2510
|
+
if (arg === "--cwd") {
|
|
2511
|
+
const value = argv[index + 1];
|
|
2512
|
+
if (value === void 0) {
|
|
2513
|
+
throw new CliUsageError("--cwd requires a path");
|
|
2514
|
+
}
|
|
2515
|
+
cwd = value;
|
|
2516
|
+
index += 1;
|
|
2517
|
+
continue;
|
|
2518
|
+
}
|
|
2519
|
+
if (arg === "--quiet") {
|
|
2520
|
+
quiet = true;
|
|
2521
|
+
continue;
|
|
2522
|
+
}
|
|
2523
|
+
if (arg === "--no-session-log") {
|
|
2524
|
+
noSessionLog = true;
|
|
2525
|
+
continue;
|
|
2526
|
+
}
|
|
2527
|
+
command.push(arg);
|
|
2528
|
+
}
|
|
2529
|
+
return { cwd, quiet, noSessionLog, command };
|
|
2530
|
+
}
|
|
2531
|
+
async function runInit(options, args, io) {
|
|
2532
|
+
const parsed = parseInitArgs(args, io);
|
|
2533
|
+
if (parsed === void 0) {
|
|
2534
|
+
return;
|
|
2535
|
+
}
|
|
2536
|
+
const result = await initStore(parsed.root, {
|
|
2537
|
+
claude: parsed.claude,
|
|
2538
|
+
codex: parsed.codex
|
|
2539
|
+
});
|
|
2540
|
+
if (options.quiet) {
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
2543
|
+
write(io.stdout, `Initialized CodeSteward store at ${result.paths.store}
|
|
2544
|
+
`);
|
|
2545
|
+
if (!parsed.claude && !parsed.codex) {
|
|
2546
|
+
write(io.stdout, "Agent runtime bootstrap skipped. Pass --claude, --codex, or both to install runtime assets.\n");
|
|
2547
|
+
}
|
|
2548
|
+
if (result.created.length > 0) {
|
|
2549
|
+
renderPathGroup("Created", result.created, io);
|
|
2550
|
+
}
|
|
2551
|
+
if (result.existing.length > 0) {
|
|
2552
|
+
renderPathGroup("Already present", result.existing, io);
|
|
2553
|
+
}
|
|
2554
|
+
if (result.updated.length > 0) {
|
|
2555
|
+
renderPathGroup("Updated", result.updated, io);
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
function renderPathGroup(title, items, io) {
|
|
2559
|
+
if (items.length === 0) {
|
|
2560
|
+
return;
|
|
2561
|
+
}
|
|
2562
|
+
write(io.stdout, `
|
|
2563
|
+
${title}:
|
|
2564
|
+
`);
|
|
2565
|
+
for (const item of items) {
|
|
2566
|
+
write(io.stdout, `- ${item}
|
|
2567
|
+
`);
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
function parseInitArgs(args, io) {
|
|
2571
|
+
let root;
|
|
2572
|
+
let claude = false;
|
|
2573
|
+
let codex = false;
|
|
2574
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
2575
|
+
const arg = args[index];
|
|
2576
|
+
if (arg === "--root") {
|
|
2577
|
+
const value = args[index + 1];
|
|
2578
|
+
if (value === void 0) {
|
|
2579
|
+
return initUsageError(io);
|
|
2580
|
+
}
|
|
2581
|
+
root = value;
|
|
2582
|
+
index += 1;
|
|
2583
|
+
continue;
|
|
2584
|
+
}
|
|
2585
|
+
if (arg === "--claude") {
|
|
2586
|
+
claude = true;
|
|
2587
|
+
continue;
|
|
2588
|
+
}
|
|
2589
|
+
if (arg === "--codex") {
|
|
2590
|
+
codex = true;
|
|
2591
|
+
continue;
|
|
2592
|
+
}
|
|
2593
|
+
return initUsageError(io);
|
|
2594
|
+
}
|
|
2595
|
+
if (root === void 0) {
|
|
2596
|
+
return initUsageError(io);
|
|
2597
|
+
}
|
|
2598
|
+
return { root, claude, codex };
|
|
2599
|
+
}
|
|
2600
|
+
function initUsageError(io) {
|
|
2601
|
+
write(io.stderr, "Usage: codesteward init --root <path> [--claude] [--codex]\n");
|
|
2602
|
+
io.exitCode = 64;
|
|
2603
|
+
return void 0;
|
|
2604
|
+
}
|
|
2605
|
+
async function runTags(options, args, io) {
|
|
2606
|
+
if (args.length > 0) {
|
|
2607
|
+
write(io.stderr, `Usage: codesteward tags
|
|
2608
|
+
`);
|
|
2609
|
+
io.exitCode = 64;
|
|
2610
|
+
return;
|
|
2611
|
+
}
|
|
2612
|
+
const paths = await requireStore(options.cwd, io);
|
|
2613
|
+
if (paths === void 0) {
|
|
2614
|
+
return;
|
|
2615
|
+
}
|
|
2616
|
+
const vocabulary = await readTagVocabulary(paths.tags);
|
|
2617
|
+
if (!validateVocabularyForListing(vocabulary.errors, io)) {
|
|
2618
|
+
return;
|
|
2619
|
+
}
|
|
2620
|
+
if (options.quiet) {
|
|
2621
|
+
return;
|
|
2622
|
+
}
|
|
2623
|
+
write(io.stdout, "Domains:\n");
|
|
2624
|
+
renderVocabularyDefinitions(vocabulary.domains, "domains", io);
|
|
2625
|
+
write(io.stdout, "\nTags:\n");
|
|
2626
|
+
renderVocabularyDefinitions(vocabulary.tags, "tags", io);
|
|
2627
|
+
}
|
|
2628
|
+
function validateVocabularyForListing(errors, io) {
|
|
2629
|
+
if (errors.length === 0) {
|
|
2630
|
+
return true;
|
|
2631
|
+
}
|
|
2632
|
+
for (const error of errors) {
|
|
2633
|
+
write(io.stderr, `${error}
|
|
2634
|
+
`);
|
|
2635
|
+
}
|
|
2636
|
+
io.exitCode = 1;
|
|
2637
|
+
return false;
|
|
2638
|
+
}
|
|
2639
|
+
function renderVocabularyDefinitions(definitions, label, io) {
|
|
2640
|
+
if (definitions.length === 0) {
|
|
2641
|
+
write(io.stdout, `No ${label} defined.
|
|
2642
|
+
`);
|
|
2643
|
+
return;
|
|
2644
|
+
}
|
|
2645
|
+
for (const definition of definitions) {
|
|
2646
|
+
write(io.stdout, `${definition.name}`);
|
|
2647
|
+
if (definition.description.length > 0) {
|
|
2648
|
+
write(io.stdout, ` - ${definition.description.replace(/\s+/g, " ")}`);
|
|
2649
|
+
}
|
|
2650
|
+
write(io.stdout, "\n");
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
function unknownQueryDomain(domains, domain) {
|
|
2654
|
+
if (domain === void 0 || domain === "all" || domains.length === 0) {
|
|
2655
|
+
return void 0;
|
|
2656
|
+
}
|
|
2657
|
+
const knownDomains = new Set(domains.map((item) => item.name));
|
|
2658
|
+
return knownDomains.has(domain) ? void 0 : domain;
|
|
2659
|
+
}
|
|
2660
|
+
async function requireStore(cwd, io) {
|
|
2661
|
+
const paths = await discoverStore(cwd);
|
|
2662
|
+
if (paths === void 0) {
|
|
2663
|
+
write(io.stderr, "No .codesteward store found. Run codesteward init --root <path> first.\n");
|
|
2664
|
+
io.exitCode = 1;
|
|
2665
|
+
return void 0;
|
|
2666
|
+
}
|
|
2667
|
+
return paths;
|
|
2668
|
+
}
|
|
2669
|
+
async function requireStoreAtRoot(root, io) {
|
|
2670
|
+
const paths = getStorePaths(root);
|
|
2671
|
+
if (!await pathExists(paths.store)) {
|
|
2672
|
+
write(io.stderr, `No .codesteward store found at ${paths.root}. Run codesteward init --root ${paths.root} first.
|
|
2673
|
+
`);
|
|
2674
|
+
io.exitCode = 1;
|
|
2675
|
+
return void 0;
|
|
2676
|
+
}
|
|
2677
|
+
return paths;
|
|
2678
|
+
}
|
|
2679
|
+
var CliUsageError = class extends Error {
|
|
2680
|
+
};
|
|
2681
|
+
function write(stream, text) {
|
|
2682
|
+
stream.write(text);
|
|
2683
|
+
}
|
|
2684
|
+
function formatStatusLabel(status) {
|
|
2685
|
+
return `${status.slice(0, 1).toUpperCase()}${status.slice(1)}`;
|
|
2686
|
+
}
|
|
2687
|
+
function isNodeError4(error) {
|
|
2688
|
+
return error instanceof Error && "code" in error;
|
|
2689
|
+
}
|
|
2690
|
+
if (require.main === module) {
|
|
2691
|
+
main(process.argv.slice(2), process).catch((error) => {
|
|
2692
|
+
if (error instanceof CliUsageError) {
|
|
2693
|
+
process.stderr.write(`${error.message}
|
|
2694
|
+
|
|
2695
|
+
${usage}`);
|
|
2696
|
+
process.exitCode = 64;
|
|
2697
|
+
return;
|
|
2698
|
+
}
|
|
2699
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2700
|
+
process.stderr.write(`${message}
|
|
2701
|
+
`);
|
|
2702
|
+
process.exitCode = 1;
|
|
2703
|
+
});
|
|
2704
|
+
}
|
|
2705
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2706
|
+
0 && (module.exports = {
|
|
2707
|
+
bootstrapCommand,
|
|
2708
|
+
main,
|
|
2709
|
+
runBootstrapCommand
|
|
2710
|
+
});
|
|
2711
|
+
//# sourceMappingURL=main.js.map
|