contract-driven-delivery 1.9.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +383 -143
- package/assets/CLAUDE.template.md +31 -204
- package/assets/agents/backend-engineer.md +19 -1
- package/assets/agents/change-classifier.md +44 -8
- package/assets/agents/ci-cd-gatekeeper.md +13 -0
- package/assets/agents/contract-reviewer.md +13 -0
- package/assets/agents/e2e-resilience-engineer.md +13 -0
- package/assets/agents/frontend-engineer.md +19 -1
- package/assets/agents/monkey-test-engineer.md +13 -0
- package/assets/agents/qa-reviewer.md +13 -0
- package/assets/agents/repo-context-scanner.md +3 -0
- package/assets/agents/spec-architect.md +41 -31
- package/assets/agents/spec-drift-auditor.md +21 -19
- package/assets/agents/stress-soak-engineer.md +13 -0
- package/assets/agents/test-strategist.md +36 -26
- package/assets/ci-templates/conda.yml +1 -1
- package/assets/{ci/github-actions → github-workflows}/contract-driven-gates.yml +12 -17
- package/assets/hooks/pre-commit +1 -1
- package/assets/skills/cdd-close/SKILL.md +123 -0
- package/assets/skills/cdd-init/SKILL.md +6 -0
- package/assets/skills/cdd-new/SKILL.md +108 -24
- package/assets/skills/cdd-resume/SKILL.md +86 -0
- package/assets/skills/contract-driven-delivery/templates/change-classification.md +18 -11
- package/assets/skills/contract-driven-delivery/templates/design.md +16 -13
- package/assets/skills/contract-driven-delivery/templates/tasks.md +7 -0
- package/assets/skills/contract-driven-delivery/templates/test-plan.md +17 -23
- package/assets/specs-templates/change-classification.md +18 -11
- package/assets/specs-templates/design.md +16 -13
- package/assets/specs-templates/tasks.md +7 -0
- package/assets/specs-templates/test-plan.md +17 -23
- package/dist/cli/index.js +508 -41
- package/package.json +8 -5
package/dist/cli/index.js
CHANGED
|
@@ -1,7 +1,368 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// src/utils/logger.ts
|
|
12
|
+
var RESET, CYAN, GREEN, YELLOW, RED, DIM, log;
|
|
13
|
+
var init_logger = __esm({
|
|
14
|
+
"src/utils/logger.ts"() {
|
|
15
|
+
"use strict";
|
|
16
|
+
RESET = "\x1B[0m";
|
|
17
|
+
CYAN = "\x1B[36m";
|
|
18
|
+
GREEN = "\x1B[32m";
|
|
19
|
+
YELLOW = "\x1B[33m";
|
|
20
|
+
RED = "\x1B[31m";
|
|
21
|
+
DIM = "\x1B[2m";
|
|
22
|
+
log = {
|
|
23
|
+
info(msg) {
|
|
24
|
+
console.log(`${CYAN}\u2139${RESET} ${msg}`);
|
|
25
|
+
},
|
|
26
|
+
ok(msg) {
|
|
27
|
+
console.log(`${GREEN}\u2713${RESET} ${msg}`);
|
|
28
|
+
},
|
|
29
|
+
warn(msg) {
|
|
30
|
+
console.log(`${YELLOW}\u26A0${RESET} ${msg}`);
|
|
31
|
+
},
|
|
32
|
+
error(msg) {
|
|
33
|
+
console.error(`${RED}\u2717${RESET} ${msg}`);
|
|
34
|
+
},
|
|
35
|
+
dim(msg) {
|
|
36
|
+
console.log(`${DIM} ${msg}${RESET}`);
|
|
37
|
+
},
|
|
38
|
+
blank() {
|
|
39
|
+
console.log("");
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// src/commands/archive.ts
|
|
46
|
+
var archive_exports = {};
|
|
47
|
+
__export(archive_exports, {
|
|
48
|
+
archive: () => archive
|
|
49
|
+
});
|
|
50
|
+
import { join as join10 } from "path";
|
|
51
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync4, renameSync, readFileSync as readFileSync6, writeFileSync as writeFileSync3, appendFileSync, cpSync as cpSync2, rmSync as rmSync2 } from "fs";
|
|
52
|
+
async function archive(changeId) {
|
|
53
|
+
const cwd = process.cwd();
|
|
54
|
+
const changeDir = join10(cwd, "specs", "changes", changeId);
|
|
55
|
+
const archiveYear = (/* @__PURE__ */ new Date()).getFullYear().toString();
|
|
56
|
+
const archiveBase = join10(cwd, "specs", "archive", archiveYear);
|
|
57
|
+
const archiveDir = join10(archiveBase, changeId);
|
|
58
|
+
const indexPath = join10(cwd, "specs", "archive", "INDEX.md");
|
|
59
|
+
if (!existsSync9(changeDir)) {
|
|
60
|
+
log.error(`Change not found: specs/changes/${changeId}`);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
if (existsSync9(archiveDir)) {
|
|
64
|
+
log.error(`Already archived: specs/archive/${archiveYear}/${changeId}`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
const tasksPath = join10(changeDir, "tasks.md");
|
|
68
|
+
if (existsSync9(tasksPath)) {
|
|
69
|
+
const content = readFileSync6(tasksPath, "utf8");
|
|
70
|
+
if (content.includes("status: gate-blocked")) {
|
|
71
|
+
log.warn("tasks.md has status: gate-blocked \u2014 archiving anyway (change was paused).");
|
|
72
|
+
}
|
|
73
|
+
const pending = (content.match(/^\s*-\s*\[ \]/gm) || []).length;
|
|
74
|
+
if (pending > 0) {
|
|
75
|
+
log.warn(`${pending} task(s) still pending ([ ]). Archive anyway.`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (!existsSync9(archiveBase)) {
|
|
79
|
+
mkdirSync4(archiveBase, { recursive: true });
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
renameSync(changeDir, archiveDir);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
if (err.code === "EXDEV") {
|
|
85
|
+
cpSync2(changeDir, archiveDir, { recursive: true });
|
|
86
|
+
rmSync2(changeDir, { recursive: true, force: true });
|
|
87
|
+
} else {
|
|
88
|
+
throw err;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
log.ok(`Archived: specs/changes/${changeId} \u2192 specs/archive/${archiveYear}/${changeId}`);
|
|
92
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
93
|
+
const indexLine = `| ${changeId} | ${archiveYear} | ${today} | specs/archive/${archiveYear}/${changeId}/ |
|
|
94
|
+
`;
|
|
95
|
+
if (!existsSync9(indexPath)) {
|
|
96
|
+
writeFileSync3(indexPath, `# Archive Index
|
|
97
|
+
|
|
98
|
+
| change-id | year | archived-date | path |
|
|
99
|
+
|---|---|---|---|
|
|
100
|
+
${indexLine}`, "utf8");
|
|
101
|
+
} else {
|
|
102
|
+
appendFileSync(indexPath, indexLine, "utf8");
|
|
103
|
+
}
|
|
104
|
+
log.ok(`Index updated: specs/archive/INDEX.md`);
|
|
105
|
+
log.blank();
|
|
106
|
+
log.info(`Next: promote durable learnings from archive.md to contracts/ or CLAUDE.md`);
|
|
107
|
+
}
|
|
108
|
+
var init_archive = __esm({
|
|
109
|
+
"src/commands/archive.ts"() {
|
|
110
|
+
"use strict";
|
|
111
|
+
init_logger();
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// src/commands/abandon.ts
|
|
116
|
+
var abandon_exports = {};
|
|
117
|
+
__export(abandon_exports, {
|
|
118
|
+
abandon: () => abandon
|
|
119
|
+
});
|
|
120
|
+
import { join as join11 } from "path";
|
|
121
|
+
import { existsSync as existsSync10, readFileSync as readFileSync7, writeFileSync as writeFileSync4, appendFileSync as appendFileSync2, mkdirSync as mkdirSync5 } from "fs";
|
|
122
|
+
async function abandon(changeId, opts) {
|
|
123
|
+
const cwd = process.cwd();
|
|
124
|
+
const changeDir = join11(cwd, "specs", "changes", changeId);
|
|
125
|
+
const tasksPath = join11(changeDir, "tasks.md");
|
|
126
|
+
if (!existsSync10(changeDir)) {
|
|
127
|
+
log.error(`Change not found: specs/changes/${changeId}`);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
if (existsSync10(tasksPath)) {
|
|
131
|
+
let content = readFileSync7(tasksPath, "utf8");
|
|
132
|
+
if (content.match(/^status:/m)) {
|
|
133
|
+
content = content.replace(/^status: .*/m, "status: abandoned");
|
|
134
|
+
} else {
|
|
135
|
+
content = `---
|
|
136
|
+
change-id: ${changeId}
|
|
137
|
+
status: abandoned
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
` + content;
|
|
141
|
+
}
|
|
142
|
+
writeFileSync4(tasksPath, content, "utf8");
|
|
143
|
+
}
|
|
144
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
145
|
+
const archiveDir = join11(cwd, "specs", "archive");
|
|
146
|
+
const indexPath = join11(archiveDir, "INDEX.md");
|
|
147
|
+
const reason = opts.reason ?? "no reason given";
|
|
148
|
+
const indexLine = `| ${changeId} | abandoned | ${today} | ${reason} |
|
|
149
|
+
`;
|
|
150
|
+
if (!existsSync10(archiveDir)) {
|
|
151
|
+
mkdirSync5(archiveDir, { recursive: true });
|
|
152
|
+
}
|
|
153
|
+
if (!existsSync10(indexPath)) {
|
|
154
|
+
writeFileSync4(indexPath, `# Archive Index
|
|
155
|
+
|
|
156
|
+
| change-id | status | date | notes |
|
|
157
|
+
|---|---|---|---|
|
|
158
|
+
${indexLine}`, "utf8");
|
|
159
|
+
} else {
|
|
160
|
+
appendFileSync2(indexPath, indexLine, "utf8");
|
|
161
|
+
}
|
|
162
|
+
log.ok(`Change ${changeId} marked as abandoned.`);
|
|
163
|
+
log.info(`specs/changes/${changeId}/ remains on disk (git history preserved).`);
|
|
164
|
+
log.info(`Run \`cdd-kit archive ${changeId}\` to physically move it, or leave it for git history.`);
|
|
165
|
+
}
|
|
166
|
+
var init_abandon = __esm({
|
|
167
|
+
"src/commands/abandon.ts"() {
|
|
168
|
+
"use strict";
|
|
169
|
+
init_logger();
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// src/commands/migrate.ts
|
|
174
|
+
var migrate_exports = {};
|
|
175
|
+
__export(migrate_exports, {
|
|
176
|
+
migrate: () => migrate
|
|
177
|
+
});
|
|
178
|
+
import { join as join12 } from "path";
|
|
179
|
+
import { existsSync as existsSync11, readdirSync as readdirSync6, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "fs";
|
|
180
|
+
function migrateOne(changeId, changeDir, dryRun) {
|
|
181
|
+
const changed = [];
|
|
182
|
+
const warnings = [];
|
|
183
|
+
const tasksPath = join12(changeDir, "tasks.md");
|
|
184
|
+
if (existsSync11(tasksPath)) {
|
|
185
|
+
let content = readFileSync8(tasksPath, "utf8");
|
|
186
|
+
const norm = content.replace(/\r\n/g, "\n");
|
|
187
|
+
let modified = false;
|
|
188
|
+
if (!norm.startsWith("---")) {
|
|
189
|
+
const bareStatusMatch = norm.match(/^status:\s*(\S+)/m);
|
|
190
|
+
const inferredStatus = bareStatusMatch ? bareStatusMatch[1] : "in-progress";
|
|
191
|
+
if (bareStatusMatch) {
|
|
192
|
+
content = content.replace(/^status:\s*\S+[ \t]*\n?/m, "");
|
|
193
|
+
}
|
|
194
|
+
content = `---
|
|
195
|
+
change-id: ${changeId}
|
|
196
|
+
status: ${inferredStatus}
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
` + content;
|
|
200
|
+
modified = true;
|
|
201
|
+
}
|
|
202
|
+
if (!content.includes("[x]=done")) {
|
|
203
|
+
content = content.replace(
|
|
204
|
+
/^(---\n[\s\S]*?---\n)/,
|
|
205
|
+
`$1
|
|
206
|
+
<!-- [x]=done [-]=N/A [ ]=pending -->
|
|
207
|
+
`
|
|
208
|
+
);
|
|
209
|
+
modified = true;
|
|
210
|
+
}
|
|
211
|
+
if (modified) {
|
|
212
|
+
changed.push("tasks.md: added YAML frontmatter (status: in-progress) + legend comment");
|
|
213
|
+
if (!dryRun)
|
|
214
|
+
writeFileSync5(tasksPath, content, "utf8");
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
warnings.push("tasks.md not found \u2014 skipping frontmatter migration");
|
|
218
|
+
}
|
|
219
|
+
const classifPath = join12(changeDir, "change-classification.md");
|
|
220
|
+
if (existsSync11(classifPath)) {
|
|
221
|
+
const content = readFileSync8(classifPath, "utf8");
|
|
222
|
+
const hasNewTierFormat = /^## Tier\s*\n\s*-\s*\d\s*$/m.test(content);
|
|
223
|
+
if (!hasNewTierFormat) {
|
|
224
|
+
const oldMatch = content.match(/\*\*Tier[:\*]+\s*(?:Tier\s*)?(\d)/i) ?? content.match(/^-?\s*Tier:\s*(?:Tier\s*)?(\d)/mi);
|
|
225
|
+
const detectedTier = oldMatch ? oldMatch[1] : null;
|
|
226
|
+
if (detectedTier) {
|
|
227
|
+
const addition = `
|
|
228
|
+
## Tier
|
|
229
|
+
- ${detectedTier}
|
|
230
|
+
`;
|
|
231
|
+
if (!content.includes("\n## Tier\n")) {
|
|
232
|
+
changed.push(
|
|
233
|
+
`change-classification.md: appended "## Tier\\n- ${detectedTier}" (converted from old format)`
|
|
234
|
+
);
|
|
235
|
+
if (!dryRun)
|
|
236
|
+
writeFileSync5(classifPath, content + addition, "utf8");
|
|
237
|
+
}
|
|
238
|
+
} else {
|
|
239
|
+
warnings.push(
|
|
240
|
+
"change-classification.md: could not detect tier (no **Tier:** N or ## Tier N found). gate tier-based agent-log checks will be skipped for this change."
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return { changed, warnings };
|
|
246
|
+
}
|
|
247
|
+
async function migrate(changeId, opts = {}) {
|
|
248
|
+
const cwd = process.cwd();
|
|
249
|
+
const dryRun = opts.dryRun ?? false;
|
|
250
|
+
const idsToMigrate = [];
|
|
251
|
+
if (opts.all) {
|
|
252
|
+
const changesDir = join12(cwd, "specs", "changes");
|
|
253
|
+
if (!existsSync11(changesDir)) {
|
|
254
|
+
log.info("No specs/changes/ directory found \u2014 nothing to migrate.");
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
idsToMigrate.push(
|
|
258
|
+
...readdirSync6(changesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name)
|
|
259
|
+
);
|
|
260
|
+
} else if (changeId) {
|
|
261
|
+
const specificDir = join12(cwd, "specs", "changes", changeId);
|
|
262
|
+
if (!existsSync11(specificDir)) {
|
|
263
|
+
log.error(`Change not found: specs/changes/${changeId}`);
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
idsToMigrate.push(changeId);
|
|
267
|
+
} else {
|
|
268
|
+
log.error("Usage: cdd-kit migrate <change-id> | cdd-kit migrate --all [--dry-run]");
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
if (idsToMigrate.length === 0) {
|
|
272
|
+
log.info("No changes found to migrate.");
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (dryRun) {
|
|
276
|
+
log.info("Dry run \u2014 no files will be written.");
|
|
277
|
+
log.blank();
|
|
278
|
+
}
|
|
279
|
+
let migratedCount = 0;
|
|
280
|
+
let upToDateCount = 0;
|
|
281
|
+
for (const id of idsToMigrate) {
|
|
282
|
+
const changeDir = join12(cwd, "specs", "changes", id);
|
|
283
|
+
if (!existsSync11(changeDir)) {
|
|
284
|
+
log.warn(` ${id}: directory not found \u2014 skipping`);
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const { changed, warnings } = migrateOne(id, changeDir, dryRun);
|
|
288
|
+
if (changed.length > 0) {
|
|
289
|
+
log.ok(` ${id}: migrated`);
|
|
290
|
+
for (const c of changed)
|
|
291
|
+
log.info(` + ${c}`);
|
|
292
|
+
migratedCount++;
|
|
293
|
+
} else {
|
|
294
|
+
log.info(` ${id}: already up to date`);
|
|
295
|
+
upToDateCount++;
|
|
296
|
+
}
|
|
297
|
+
for (const w of warnings) {
|
|
298
|
+
log.warn(` ${id}: ${w}`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
log.blank();
|
|
302
|
+
if (dryRun) {
|
|
303
|
+
log.info(`Dry run complete: ${migratedCount} change(s) would be updated, ${upToDateCount} already up to date.`);
|
|
304
|
+
} else {
|
|
305
|
+
log.ok(`Migration complete: ${migratedCount} updated, ${upToDateCount} already up to date.`);
|
|
306
|
+
if (migratedCount > 0) {
|
|
307
|
+
log.info('Next: git add specs/changes/ && git commit -m "chore: migrate changes to v1.11.0 format"');
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
var init_migrate = __esm({
|
|
312
|
+
"src/commands/migrate.ts"() {
|
|
313
|
+
"use strict";
|
|
314
|
+
init_logger();
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// src/commands/list-changes.ts
|
|
319
|
+
var list_changes_exports = {};
|
|
320
|
+
__export(list_changes_exports, {
|
|
321
|
+
listChanges: () => listChanges
|
|
322
|
+
});
|
|
323
|
+
import { join as join13 } from "path";
|
|
324
|
+
import { existsSync as existsSync12, readdirSync as readdirSync7, readFileSync as readFileSync9 } from "fs";
|
|
325
|
+
async function listChanges() {
|
|
326
|
+
const cwd = process.cwd();
|
|
327
|
+
const changesDir = join13(cwd, "specs", "changes");
|
|
328
|
+
log.blank();
|
|
329
|
+
const active = [];
|
|
330
|
+
if (existsSync12(changesDir)) {
|
|
331
|
+
active.push(...readdirSync7(changesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name));
|
|
332
|
+
}
|
|
333
|
+
if (active.length === 0) {
|
|
334
|
+
log.info("No active changes in specs/changes/");
|
|
335
|
+
} else {
|
|
336
|
+
log.info("Active changes:");
|
|
337
|
+
for (const id of active) {
|
|
338
|
+
const tasksPath = join13(changesDir, id, "tasks.md");
|
|
339
|
+
let status = "in-progress";
|
|
340
|
+
let pending = 0;
|
|
341
|
+
if (existsSync12(tasksPath)) {
|
|
342
|
+
const content = readFileSync9(tasksPath, "utf8");
|
|
343
|
+
if (content.includes("status: gate-blocked"))
|
|
344
|
+
status = "gate-blocked";
|
|
345
|
+
else if (content.includes("status: abandoned"))
|
|
346
|
+
status = "abandoned";
|
|
347
|
+
pending = (content.match(/^\s*-\s*\[ \]/gm) || []).length;
|
|
348
|
+
}
|
|
349
|
+
const pendingStr = pending > 0 ? ` (${pending} pending)` : "";
|
|
350
|
+
log.info(` ${id} [${status}]${pendingStr}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
log.blank();
|
|
354
|
+
}
|
|
355
|
+
var init_list_changes = __esm({
|
|
356
|
+
"src/commands/list-changes.ts"() {
|
|
357
|
+
"use strict";
|
|
358
|
+
init_logger();
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
|
|
1
362
|
// src/cli/index.ts
|
|
2
|
-
import { readFileSync as
|
|
363
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
3
364
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
4
|
-
import { dirname as dirname3, join as
|
|
365
|
+
import { dirname as dirname3, join as join14 } from "path";
|
|
5
366
|
import { Command } from "commander";
|
|
6
367
|
|
|
7
368
|
// src/commands/init.ts
|
|
@@ -26,12 +387,14 @@ var ASSET = {
|
|
|
26
387
|
specsTemplates: join(ASSETS_DIR, "specs-templates"),
|
|
27
388
|
testsTemplates: join(ASSETS_DIR, "tests-templates"),
|
|
28
389
|
ci: join(ASSETS_DIR, "ci"),
|
|
390
|
+
githubWorkflows: join(ASSETS_DIR, "github-workflows"),
|
|
29
391
|
hooks: join(ASSETS_DIR, "hooks"),
|
|
30
392
|
claudeTemplate: join(ASSETS_DIR, "CLAUDE.template.md"),
|
|
31
393
|
agentsTemplate: join(ASSETS_DIR, "AGENTS.template.md")
|
|
32
394
|
};
|
|
33
395
|
|
|
34
396
|
// src/utils/copy.ts
|
|
397
|
+
init_logger();
|
|
35
398
|
import {
|
|
36
399
|
mkdirSync,
|
|
37
400
|
existsSync,
|
|
@@ -39,36 +402,6 @@ import {
|
|
|
39
402
|
copyFileSync
|
|
40
403
|
} from "fs";
|
|
41
404
|
import { join as join2, dirname as dirname2, relative } from "path";
|
|
42
|
-
|
|
43
|
-
// src/utils/logger.ts
|
|
44
|
-
var RESET = "\x1B[0m";
|
|
45
|
-
var CYAN = "\x1B[36m";
|
|
46
|
-
var GREEN = "\x1B[32m";
|
|
47
|
-
var YELLOW = "\x1B[33m";
|
|
48
|
-
var RED = "\x1B[31m";
|
|
49
|
-
var DIM = "\x1B[2m";
|
|
50
|
-
var log = {
|
|
51
|
-
info(msg) {
|
|
52
|
-
console.log(`${CYAN}\u2139${RESET} ${msg}`);
|
|
53
|
-
},
|
|
54
|
-
ok(msg) {
|
|
55
|
-
console.log(`${GREEN}\u2713${RESET} ${msg}`);
|
|
56
|
-
},
|
|
57
|
-
warn(msg) {
|
|
58
|
-
console.log(`${YELLOW}\u26A0${RESET} ${msg}`);
|
|
59
|
-
},
|
|
60
|
-
error(msg) {
|
|
61
|
-
console.error(`${RED}\u2717${RESET} ${msg}`);
|
|
62
|
-
},
|
|
63
|
-
dim(msg) {
|
|
64
|
-
console.log(`${DIM} ${msg}${RESET}`);
|
|
65
|
-
},
|
|
66
|
-
blank() {
|
|
67
|
-
console.log("");
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
// src/utils/copy.ts
|
|
72
405
|
function ensureDir(dir) {
|
|
73
406
|
mkdirSync(dir, { recursive: true });
|
|
74
407
|
}
|
|
@@ -140,6 +473,9 @@ function copyFileTracked(src, dest, opts = {}) {
|
|
|
140
473
|
return { written: true, created: isNew };
|
|
141
474
|
}
|
|
142
475
|
|
|
476
|
+
// src/commands/init.ts
|
|
477
|
+
init_logger();
|
|
478
|
+
|
|
143
479
|
// src/utils/stack-detect.ts
|
|
144
480
|
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
145
481
|
import { join as join3 } from "path";
|
|
@@ -224,6 +560,18 @@ function detectStack(repoRoot) {
|
|
|
224
560
|
}
|
|
225
561
|
|
|
226
562
|
// src/commands/init.ts
|
|
563
|
+
function readCondaEnvName(cwd) {
|
|
564
|
+
const envYml = join4(cwd, "environment.yml");
|
|
565
|
+
if (!existsSync3(envYml))
|
|
566
|
+
return "base";
|
|
567
|
+
try {
|
|
568
|
+
const content = readFileSync2(envYml, "utf8");
|
|
569
|
+
const match = content.match(/^name:\s*(.+)$/m);
|
|
570
|
+
return match ? match[1].trim() : "base";
|
|
571
|
+
} catch {
|
|
572
|
+
return "base";
|
|
573
|
+
}
|
|
574
|
+
}
|
|
227
575
|
function loadCiTemplate(stack) {
|
|
228
576
|
const templatePath = join4(ASSETS_DIR, "ci-templates", `${stack}.yml`);
|
|
229
577
|
if (!existsSync3(templatePath))
|
|
@@ -326,6 +674,13 @@ async function init(opts) {
|
|
|
326
674
|
);
|
|
327
675
|
track(ciCreated);
|
|
328
676
|
log.ok(`ci/ \u2014 ${ciCount} file(s) written.`);
|
|
677
|
+
const { count: wfCount, created: wfCreated } = copyDirTracked(
|
|
678
|
+
ASSET.githubWorkflows,
|
|
679
|
+
join4(cwd, ".github", "workflows"),
|
|
680
|
+
{ overwrite: opts.force, label: ".github/workflows" }
|
|
681
|
+
);
|
|
682
|
+
track(wfCreated);
|
|
683
|
+
log.ok(`.github/workflows/ \u2014 ${wfCount} file(s) written.`);
|
|
329
684
|
const detection = detectStack(cwd);
|
|
330
685
|
if (detection.polyglot) {
|
|
331
686
|
const PYTHON_STACKS = ["conda", "poetry", "uv", "pip"];
|
|
@@ -345,12 +700,17 @@ async function init(opts) {
|
|
|
345
700
|
} else {
|
|
346
701
|
log.warn("Could not detect stack \u2014 CI placeholder left in place.");
|
|
347
702
|
}
|
|
348
|
-
const ciYmlDest = join4(cwd, "
|
|
703
|
+
const ciYmlDest = join4(cwd, ".github", "workflows", "contract-driven-gates.yml");
|
|
349
704
|
if (existsSync3(ciYmlDest)) {
|
|
350
705
|
const template = loadCiTemplate(detection.primary);
|
|
351
706
|
if (template) {
|
|
352
707
|
const original = readFileSync2(ciYmlDest, "utf8");
|
|
353
|
-
|
|
708
|
+
let patched = patchFastGateYml(original, template, detection.primary);
|
|
709
|
+
if (detection.primary === "conda" && patched.includes("{{conda-env-name}}")) {
|
|
710
|
+
const envName = readCondaEnvName(cwd);
|
|
711
|
+
patched = patched.replace(/\{\{conda-env-name\}\}/g, envName);
|
|
712
|
+
log.ok(`Conda environment name set to: ${envName}`);
|
|
713
|
+
}
|
|
354
714
|
if (patched !== original) {
|
|
355
715
|
writeFileSync(ciYmlDest, patched, "utf8");
|
|
356
716
|
log.ok(`CI fast-gate patched for stack: ${detection.primary}`);
|
|
@@ -392,6 +752,7 @@ async function init(opts) {
|
|
|
392
752
|
import { join as join5 } from "path";
|
|
393
753
|
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readdirSync as readdirSync3, copyFileSync as copyFileSync2, readFileSync as readFileSync3 } from "fs";
|
|
394
754
|
import { createHash } from "crypto";
|
|
755
|
+
init_logger();
|
|
395
756
|
import { homedir as homedir2 } from "os";
|
|
396
757
|
function fileHash(filePath) {
|
|
397
758
|
const buf = readFileSync3(filePath);
|
|
@@ -506,6 +867,7 @@ async function update(opts) {
|
|
|
506
867
|
// src/commands/new-change.ts
|
|
507
868
|
import { join as join6 } from "path";
|
|
508
869
|
import { existsSync as existsSync5, readdirSync as readdirSync4 } from "fs";
|
|
870
|
+
init_logger();
|
|
509
871
|
var REQUIRED_TEMPLATES = [
|
|
510
872
|
"change-request.md",
|
|
511
873
|
"change-classification.md",
|
|
@@ -564,6 +926,7 @@ async function newChange(name, opts) {
|
|
|
564
926
|
import { join as join7 } from "path";
|
|
565
927
|
import { existsSync as existsSync6 } from "fs";
|
|
566
928
|
import { spawnSync } from "child_process";
|
|
929
|
+
init_logger();
|
|
567
930
|
var VALIDATORS = [
|
|
568
931
|
{
|
|
569
932
|
flag: "contracts",
|
|
@@ -647,6 +1010,7 @@ async function validate(opts) {
|
|
|
647
1010
|
}
|
|
648
1011
|
|
|
649
1012
|
// src/commands/gate.ts
|
|
1013
|
+
init_logger();
|
|
650
1014
|
import { existsSync as existsSync7, readFileSync as readFileSync4, readdirSync as readdirSync5 } from "fs";
|
|
651
1015
|
import { join as join8 } from "path";
|
|
652
1016
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
@@ -658,10 +1022,18 @@ var REQUIRED_FILES = [
|
|
|
658
1022
|
"tasks.md"
|
|
659
1023
|
];
|
|
660
1024
|
var TIER_PATTERN = /\b(tier\s*[0-5]|low|medium|high|critical)\b/i;
|
|
1025
|
+
var MIN_CHARS = {
|
|
1026
|
+
"change-classification.md": 200,
|
|
1027
|
+
"test-plan.md": 200,
|
|
1028
|
+
"ci-gates.md": 150,
|
|
1029
|
+
"change-request.md": 100,
|
|
1030
|
+
"tasks.md": 100
|
|
1031
|
+
};
|
|
661
1032
|
function meaningfulChars(text) {
|
|
662
1033
|
return text.split("\n").map((l) => l.trim()).filter((l) => l).filter((l) => !l.startsWith("#")).filter((l) => !/^[|\s\-:]+$/.test(l)).filter((l) => !l.startsWith("<!--")).join("").length;
|
|
663
1034
|
}
|
|
664
|
-
async function gate(changeId) {
|
|
1035
|
+
async function gate(changeId, opts = {}) {
|
|
1036
|
+
const strict = opts.strict ?? false;
|
|
665
1037
|
const cwd = process.cwd();
|
|
666
1038
|
const changeDir = join8(cwd, "specs", "changes", changeId);
|
|
667
1039
|
if (!existsSync7(changeDir)) {
|
|
@@ -669,6 +1041,7 @@ async function gate(changeId) {
|
|
|
669
1041
|
process.exit(1);
|
|
670
1042
|
}
|
|
671
1043
|
const errors = [];
|
|
1044
|
+
const warnings = [];
|
|
672
1045
|
for (const f of REQUIRED_FILES) {
|
|
673
1046
|
if (!existsSync7(join8(changeDir, f))) {
|
|
674
1047
|
errors.push(`missing required artifact: ${f}`);
|
|
@@ -677,8 +1050,9 @@ async function gate(changeId) {
|
|
|
677
1050
|
if (errors.length === 0) {
|
|
678
1051
|
for (const f of REQUIRED_FILES) {
|
|
679
1052
|
const content = readFileSync4(join8(changeDir, f), "utf8");
|
|
680
|
-
|
|
681
|
-
|
|
1053
|
+
const minChars = MIN_CHARS[f] ?? 100;
|
|
1054
|
+
if (meaningfulChars(content) < minChars) {
|
|
1055
|
+
errors.push(`${f}: appears to be a stub (< ${minChars} meaningful chars)`);
|
|
682
1056
|
}
|
|
683
1057
|
}
|
|
684
1058
|
const classifPath = join8(changeDir, "change-classification.md");
|
|
@@ -689,6 +1063,18 @@ async function gate(changeId) {
|
|
|
689
1063
|
}
|
|
690
1064
|
}
|
|
691
1065
|
}
|
|
1066
|
+
const tasksPath = join8(changeDir, "tasks.md");
|
|
1067
|
+
if (existsSync7(tasksPath)) {
|
|
1068
|
+
const tasksContent = readFileSync4(tasksPath, "utf8");
|
|
1069
|
+
const nonArchivePending = (tasksContent.match(/^\s*-\s*\[ \] (?!7\.[12])/gm) || []).length;
|
|
1070
|
+
if (nonArchivePending > 0) {
|
|
1071
|
+
if (strict) {
|
|
1072
|
+
errors.push(`${nonArchivePending} task(s) still pending (use [-] for N/A items, [x] for done). Run gate without --strict during development.`);
|
|
1073
|
+
} else {
|
|
1074
|
+
warnings.push(`${nonArchivePending} task(s) still pending (warning only in non-strict mode)`);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
692
1078
|
const agentLogDir = join8(changeDir, "agent-log");
|
|
693
1079
|
if (existsSync7(agentLogDir)) {
|
|
694
1080
|
const logFiles = readdirSync5(agentLogDir).filter((f) => f.endsWith(".md"));
|
|
@@ -706,8 +1092,69 @@ async function gate(changeId) {
|
|
|
706
1092
|
errors.push(`agent-log/${f}: status=blocked requires concrete "next-action:" line (>= 10 chars, not "none")`);
|
|
707
1093
|
}
|
|
708
1094
|
}
|
|
1095
|
+
if (strict) {
|
|
1096
|
+
const artifactsMatch = content.match(/- artifacts:([\s\S]*?)(?:\n- |\n#|$)/);
|
|
1097
|
+
if (artifactsMatch) {
|
|
1098
|
+
const artifactLines = artifactsMatch[1].split("\n").filter((l) => l.trim().startsWith("-"));
|
|
1099
|
+
for (const line of artifactLines) {
|
|
1100
|
+
const pointer = line.replace(/^\s*-\s*[\w-]+:\s*/, "").trim();
|
|
1101
|
+
const pathPart = pointer.split(":")[0];
|
|
1102
|
+
if (pathPart.includes("/") && !pointer.startsWith("http")) {
|
|
1103
|
+
const abs = join8(cwd, pathPart);
|
|
1104
|
+
if (!existsSync7(abs)) {
|
|
1105
|
+
errors.push(`agent-log/${f}: artifact pointer not found: ${pathPart}`);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
const classifPath = join8(changeDir, "change-classification.md");
|
|
1113
|
+
if (existsSync7(classifPath)) {
|
|
1114
|
+
const classificationContent = readFileSync4(classifPath, "utf8");
|
|
1115
|
+
const tierMatch = classificationContent.match(/^## Tier\s*\n\s*-\s*(\d)\s*$/m);
|
|
1116
|
+
const tier = tierMatch ? parseInt(tierMatch[1]) : null;
|
|
1117
|
+
if (tier !== null) {
|
|
1118
|
+
const agentLogFiles = readdirSync5(agentLogDir).map((f) => f.replace(".md", ""));
|
|
1119
|
+
if (tier <= 1) {
|
|
1120
|
+
for (const required of ["e2e-resilience-engineer", "monkey-test-engineer", "stress-soak-engineer"]) {
|
|
1121
|
+
if (!agentLogFiles.includes(required)) {
|
|
1122
|
+
errors.push(`Tier ${tier} change requires agent-log/${required}.md (high-risk change \u2014 E2E/monkey/stress testing mandatory)`);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
if (tier <= 3) {
|
|
1127
|
+
for (const required of ["contract-reviewer", "qa-reviewer"]) {
|
|
1128
|
+
if (!agentLogFiles.includes(required)) {
|
|
1129
|
+
errors.push(`Tier ${tier} change requires agent-log/${required}.md`);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
} else {
|
|
1136
|
+
const classifPath = join8(changeDir, "change-classification.md");
|
|
1137
|
+
if (existsSync7(classifPath)) {
|
|
1138
|
+
const classificationContent = readFileSync4(classifPath, "utf8");
|
|
1139
|
+
const tierMatch = classificationContent.match(/^## Tier\s*\n\s*-\s*(\d)\s*$/m);
|
|
1140
|
+
const tier = tierMatch ? parseInt(tierMatch[1]) : null;
|
|
1141
|
+
if (tier !== null) {
|
|
1142
|
+
if (tier <= 1) {
|
|
1143
|
+
for (const required of ["e2e-resilience-engineer", "monkey-test-engineer", "stress-soak-engineer"]) {
|
|
1144
|
+
errors.push(`Tier ${tier} change requires agent-log/${required}.md (high-risk change \u2014 E2E/monkey/stress testing mandatory)`);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
if (tier <= 3) {
|
|
1148
|
+
for (const required of ["contract-reviewer", "qa-reviewer"]) {
|
|
1149
|
+
errors.push(`Tier ${tier} change requires agent-log/${required}.md`);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
709
1153
|
}
|
|
710
1154
|
}
|
|
1155
|
+
for (const w of warnings) {
|
|
1156
|
+
log.warn(` ${w}`);
|
|
1157
|
+
}
|
|
711
1158
|
if (errors.length > 0) {
|
|
712
1159
|
log.error(`gate failed for change: ${changeId}`);
|
|
713
1160
|
for (const e of errors) {
|
|
@@ -716,7 +1163,7 @@ async function gate(changeId) {
|
|
|
716
1163
|
process.exit(1);
|
|
717
1164
|
}
|
|
718
1165
|
log.info(`gate: running contract validators for ${changeId}\u2026`);
|
|
719
|
-
const r = spawnSync2(process.execPath, [process.argv[1], "validate"], {
|
|
1166
|
+
const r = spawnSync2(process.execPath, [process.argv[1], "validate", "--contracts", "--env", "--ci", "--versions"], {
|
|
720
1167
|
cwd,
|
|
721
1168
|
stdio: "inherit"
|
|
722
1169
|
});
|
|
@@ -724,12 +1171,16 @@ async function gate(changeId) {
|
|
|
724
1171
|
log.error(`gate failed for change: ${changeId} (validators returned non-zero)`);
|
|
725
1172
|
process.exit(1);
|
|
726
1173
|
}
|
|
1174
|
+
for (const w of warnings) {
|
|
1175
|
+
log.warn(` ${w}`);
|
|
1176
|
+
}
|
|
727
1177
|
log.ok(`gate passed for change: ${changeId}`);
|
|
728
1178
|
}
|
|
729
1179
|
|
|
730
1180
|
// src/commands/install-hooks.ts
|
|
731
1181
|
import { existsSync as existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync2, chmodSync, mkdirSync as mkdirSync3 } from "fs";
|
|
732
1182
|
import { join as join9 } from "path";
|
|
1183
|
+
init_logger();
|
|
733
1184
|
var START_MARKER = "# cdd-kit-managed-block-start";
|
|
734
1185
|
var END_MARKER = "# cdd-kit-managed-block-end";
|
|
735
1186
|
async function installHooks() {
|
|
@@ -782,7 +1233,7 @@ async function installHooks() {
|
|
|
782
1233
|
|
|
783
1234
|
// src/cli/index.ts
|
|
784
1235
|
var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
785
|
-
var pkg = JSON.parse(
|
|
1236
|
+
var pkg = JSON.parse(readFileSync10(join14(__dirname2, "..", "..", "package.json"), "utf8"));
|
|
786
1237
|
var program = new Command();
|
|
787
1238
|
program.name("cdd-kit").description("Contract-Driven Delivery Kit CLI").version(pkg.version);
|
|
788
1239
|
program.command("init").description(
|
|
@@ -807,8 +1258,24 @@ program.command("validate").description("Run validation scripts (defaults to all
|
|
|
807
1258
|
versions: opts.versions
|
|
808
1259
|
})
|
|
809
1260
|
);
|
|
810
|
-
program.command("gate <change-id>").description("Run full orchestration gate for a change (required artifacts, content, tier, contracts)").action(async (id) => {
|
|
811
|
-
await gate(id);
|
|
1261
|
+
program.command("gate <change-id>").description("Run full orchestration gate for a change (required artifacts, content, tier, contracts)").option("--strict", "Treat pending tasks (except section 7) as errors, and validate artifact pointers", false).action(async (id, opts) => {
|
|
1262
|
+
await gate(id, { strict: opts.strict });
|
|
1263
|
+
});
|
|
1264
|
+
program.command("archive <change-id>").description("Move a completed change from specs/changes/ to specs/archive/<year>/").action(async (changeId) => {
|
|
1265
|
+
const { archive: archive2 } = await Promise.resolve().then(() => (init_archive(), archive_exports));
|
|
1266
|
+
await archive2(changeId);
|
|
1267
|
+
});
|
|
1268
|
+
program.command("abandon <change-id>").description("Mark a change as abandoned (updates tasks.md status, records in INDEX.md)").option("--reason <text>", "reason for abandonment").action(async (changeId, opts) => {
|
|
1269
|
+
const { abandon: abandon2 } = await Promise.resolve().then(() => (init_abandon(), abandon_exports));
|
|
1270
|
+
await abandon2(changeId, opts);
|
|
1271
|
+
});
|
|
1272
|
+
program.command("migrate [change-id]").description("Upgrade existing change directories to v1.11.0 format (tasks.md frontmatter + tier format)").option("--all", "Migrate all changes in specs/changes/", false).option("--dry-run", "Show what would change without writing files", false).action(async (changeId, opts = {}) => {
|
|
1273
|
+
const { migrate: migrate2 } = await Promise.resolve().then(() => (init_migrate(), migrate_exports));
|
|
1274
|
+
await migrate2(changeId, opts);
|
|
1275
|
+
});
|
|
1276
|
+
program.command("list").description("List active changes in specs/changes/").action(async () => {
|
|
1277
|
+
const { listChanges: listChanges2 } = await Promise.resolve().then(() => (init_list_changes(), list_changes_exports));
|
|
1278
|
+
await listChanges2();
|
|
812
1279
|
});
|
|
813
1280
|
program.command("install-hooks").description("Install pre-commit hook that runs cdd-kit gate on staged changes").action(async () => {
|
|
814
1281
|
await installHooks();
|