contract-driven-delivery 2.1.3 → 2.2.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/CHANGELOG.md +219 -0
- package/README.md +124 -1
- package/assets/CLAUDE.template.md +13 -0
- package/assets/agents/backend-engineer.md +3 -1
- package/assets/agents/frontend-engineer.md +3 -2
- package/assets/cdd/conformance.json +16 -0
- package/assets/cdd/tier-policy.json +35 -0
- package/assets/contracts/api/api-contract.md +26 -0
- package/assets/hooks/pre-tool-use-graph-first.sh +65 -0
- package/assets/skills/contract-driven-delivery/scripts/validate_api_conformance.py +671 -0
- package/assets/skills/contract-driven-delivery/scripts/validate_api_semantic.py +8 -1
- package/assets/skills/contract-driven-delivery/scripts/validate_contract_versions.py +4 -0
- package/dist/cli/index.js +2118 -491
- package/docs/adr/0001-contract-to-openapi-export.md +142 -0
- package/docs/adr/0002-schema-carrying-contract-format.md +277 -0
- package/docs/adr/0003-code-intelligence-indexing-strategy.md +110 -0
- package/docs/api-conformance.md +145 -0
- package/docs/openapi-export.md +157 -0
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -119,26 +119,26 @@ var code_map_hook_exports = {};
|
|
|
119
119
|
__export(code_map_hook_exports, {
|
|
120
120
|
installCodeMapHook: () => installCodeMapHook
|
|
121
121
|
});
|
|
122
|
-
import { existsSync as
|
|
123
|
-
import { join as
|
|
122
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, writeFileSync as writeFileSync2, chmodSync, mkdirSync as mkdirSync2 } from "fs";
|
|
123
|
+
import { join as join5 } from "path";
|
|
124
124
|
async function installCodeMapHook(cwd) {
|
|
125
|
-
const gitDir =
|
|
126
|
-
if (!
|
|
125
|
+
const gitDir = join5(cwd, ".git");
|
|
126
|
+
if (!existsSync4(gitDir)) {
|
|
127
127
|
log.warn("not a git repository (no .git/ found); skipping code-map hook installation");
|
|
128
128
|
return;
|
|
129
129
|
}
|
|
130
|
-
const hooksDir =
|
|
130
|
+
const hooksDir = join5(gitDir, "hooks");
|
|
131
131
|
mkdirSync2(hooksDir, { recursive: true });
|
|
132
|
-
const dest =
|
|
132
|
+
const dest = join5(hooksDir, "pre-commit");
|
|
133
133
|
let final;
|
|
134
|
-
if (!
|
|
134
|
+
if (!existsSync4(dest)) {
|
|
135
135
|
final = `#!/bin/sh
|
|
136
136
|
set -e
|
|
137
137
|
|
|
138
138
|
${CODE_MAP_BLOCK}
|
|
139
139
|
`;
|
|
140
140
|
} else {
|
|
141
|
-
const existing =
|
|
141
|
+
const existing = readFileSync3(dest, "utf8");
|
|
142
142
|
const startIdx = existing.indexOf(START_MARKER);
|
|
143
143
|
const endIdx = existing.indexOf(END_MARKER);
|
|
144
144
|
if (startIdx >= 0 && endIdx > startIdx) {
|
|
@@ -150,15 +150,15 @@ ${CODE_MAP_BLOCK}
|
|
|
150
150
|
final = trimmed + "\n\n" + CODE_MAP_BLOCK + "\n";
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
|
-
|
|
153
|
+
writeFileSync2(dest, final, "utf8");
|
|
154
154
|
try {
|
|
155
155
|
chmodSync(dest, 493);
|
|
156
156
|
} catch {
|
|
157
157
|
}
|
|
158
158
|
try {
|
|
159
|
-
const cddDir =
|
|
159
|
+
const cddDir = join5(cwd, ".cdd");
|
|
160
160
|
mkdirSync2(cddDir, { recursive: true });
|
|
161
|
-
|
|
161
|
+
writeFileSync2(join5(cddDir, ".hooks-installed"), `installed: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
162
162
|
`, "utf8");
|
|
163
163
|
} catch {
|
|
164
164
|
}
|
|
@@ -192,27 +192,217 @@ ${END_MARKER}`;
|
|
|
192
192
|
}
|
|
193
193
|
});
|
|
194
194
|
|
|
195
|
-
// src/
|
|
196
|
-
|
|
195
|
+
// src/commands/install-agent-hooks.ts
|
|
196
|
+
var install_agent_hooks_exports = {};
|
|
197
|
+
__export(install_agent_hooks_exports, {
|
|
198
|
+
installAgentHooks: () => installAgentHooks
|
|
199
|
+
});
|
|
200
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3, copyFileSync as copyFileSync2, chmodSync as chmodSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
197
201
|
import { join as join6 } from "path";
|
|
202
|
+
async function installAgentHooks(opts = {}) {
|
|
203
|
+
const mode = opts.graphFirst ?? "advisory";
|
|
204
|
+
if (mode !== "advisory" && mode !== "strict") {
|
|
205
|
+
log.error(`invalid mode: ${mode}. Use 'advisory' or 'strict'.`);
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
const cwd = process.cwd();
|
|
209
|
+
const srcHook = join6(ASSET.hooks, HOOK_FILENAME);
|
|
210
|
+
if (!existsSync5(srcHook)) {
|
|
211
|
+
if (opts.fromInit) {
|
|
212
|
+
log.warn(`graph-first hook not armed: bundled hook missing (${srcHook}). Reinstall the package, then run \`cdd-kit install-agent-hooks\`.`);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
log.error(`bundled hook not found: ${srcHook}. Reinstall the cdd-kit package.`);
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
const destHook = join6(cwd, HOOK_REL_PATH);
|
|
219
|
+
mkdirSync3(join6(cwd, ".claude", "hooks"), { recursive: true });
|
|
220
|
+
copyFileSync2(srcHook, destHook);
|
|
221
|
+
try {
|
|
222
|
+
chmodSync2(destHook, 493);
|
|
223
|
+
} catch {
|
|
224
|
+
}
|
|
225
|
+
const settingsPath = join6(cwd, SETTINGS_REL_PATH);
|
|
226
|
+
let settings = {};
|
|
227
|
+
if (existsSync5(settingsPath)) {
|
|
228
|
+
try {
|
|
229
|
+
settings = JSON.parse(readFileSync4(settingsPath, "utf8"));
|
|
230
|
+
} catch (err) {
|
|
231
|
+
if (opts.fromInit) {
|
|
232
|
+
log.warn(`graph-first hook not armed: ${SETTINGS_REL_PATH} is not valid JSON (${err.message}). Fix it, then run \`cdd-kit install-agent-hooks\`.`);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
log.error(`${SETTINGS_REL_PATH} is not valid JSON: ${err.message}. Fix or remove it, then re-run.`);
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
if (typeof settings !== "object" || settings === null || Array.isArray(settings)) {
|
|
239
|
+
if (opts.fromInit) {
|
|
240
|
+
log.warn(`graph-first hook not armed: ${SETTINGS_REL_PATH} must be a JSON object. Fix it, then run \`cdd-kit install-agent-hooks\`.`);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
log.error(`${SETTINGS_REL_PATH} must be a JSON object.`);
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
settings.hooks = settings.hooks ?? {};
|
|
248
|
+
const preTool = Array.isArray(settings.hooks.PreToolUse) ? settings.hooks.PreToolUse : [];
|
|
249
|
+
const isOurHandler = (h) => typeof h?.command === "string" && h.command.includes(HOOK_MARKER);
|
|
250
|
+
const preserved = [];
|
|
251
|
+
for (const e of preTool) {
|
|
252
|
+
if (typeof e?.command === "string" && e.command.includes(HOOK_MARKER) && !Array.isArray(e?.hooks)) {
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (Array.isArray(e?.hooks) && e.hooks.some(isOurHandler)) {
|
|
256
|
+
const others = e.hooks.filter((h) => !isOurHandler(h));
|
|
257
|
+
if (others.length === 0)
|
|
258
|
+
continue;
|
|
259
|
+
preserved.push({ ...e, hooks: others });
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
preserved.push(e);
|
|
263
|
+
}
|
|
264
|
+
const invoke = `./${HOOK_REL_PATH}`;
|
|
265
|
+
const command = mode === "strict" ? `CDD_GRAPH_FIRST_STRICT=1 ${invoke}` : invoke;
|
|
266
|
+
preserved.push({ matcher: "Read", hooks: [{ type: "command", command }] });
|
|
267
|
+
settings.hooks.PreToolUse = preserved;
|
|
268
|
+
mkdirSync3(join6(cwd, ".claude"), { recursive: true });
|
|
269
|
+
writeFileSync3(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf8");
|
|
270
|
+
log.ok(`graph-first PreToolUse hook installed (${mode}) at ${SETTINGS_REL_PATH}`);
|
|
271
|
+
log.info(`hook script: ${HOOK_REL_PATH}`);
|
|
272
|
+
if (mode === "advisory") {
|
|
273
|
+
log.info("advisory mode: reminds agents to use `cdd-kit index query --with-source` before Read; does not block.");
|
|
274
|
+
log.info("re-run with `--graph-first strict` to hard-block source Reads when a code-map exists.");
|
|
275
|
+
} else {
|
|
276
|
+
log.info("strict mode: blocks source-file Read when .cdd/code-map.yml exists, steering to graph/index queries.");
|
|
277
|
+
log.info("re-run with `--graph-first advisory` to downgrade to reminders only.");
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
var HOOK_FILENAME, HOOK_REL_PATH, SETTINGS_REL_PATH, HOOK_MARKER;
|
|
281
|
+
var init_install_agent_hooks = __esm({
|
|
282
|
+
"src/commands/install-agent-hooks.ts"() {
|
|
283
|
+
"use strict";
|
|
284
|
+
init_paths();
|
|
285
|
+
init_logger();
|
|
286
|
+
HOOK_FILENAME = "pre-tool-use-graph-first.sh";
|
|
287
|
+
HOOK_REL_PATH = `.claude/hooks/${HOOK_FILENAME}`;
|
|
288
|
+
SETTINGS_REL_PATH = ".claude/settings.json";
|
|
289
|
+
HOOK_MARKER = "pre-tool-use-graph-first";
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// src/commands/install-hooks.ts
|
|
294
|
+
var install_hooks_exports = {};
|
|
295
|
+
__export(install_hooks_exports, {
|
|
296
|
+
installHooks: () => installHooks
|
|
297
|
+
});
|
|
298
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, writeFileSync as writeFileSync4, chmodSync as chmodSync3, mkdirSync as mkdirSync4, statSync as statSync2 } from "fs";
|
|
299
|
+
import { join as join7, resolve } from "path";
|
|
300
|
+
import { spawnSync } from "child_process";
|
|
301
|
+
function resolveHooksDir(cwd, gitPath, fromInit) {
|
|
302
|
+
const res = spawnSync("git", ["rev-parse", "--git-path", "hooks"], { cwd, encoding: "utf8" });
|
|
303
|
+
if (res.status === 0 && res.stdout.trim()) {
|
|
304
|
+
return resolve(cwd, res.stdout.trim());
|
|
305
|
+
}
|
|
306
|
+
let gitIsDir = false;
|
|
307
|
+
try {
|
|
308
|
+
gitIsDir = statSync2(gitPath).isDirectory();
|
|
309
|
+
} catch {
|
|
310
|
+
}
|
|
311
|
+
if (gitIsDir)
|
|
312
|
+
return join7(gitPath, "hooks");
|
|
313
|
+
const why = "`.git` is a worktree/submodule pointer and git could not resolve the hooks path";
|
|
314
|
+
if (fromInit) {
|
|
315
|
+
log.warn(`pre-commit gate not armed: ${why}. Run \`cdd-kit install-hooks\` in the main checkout.`);
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
log.error(`cannot resolve hooks dir: ${why}. Run this in the main checkout.`);
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
async function installHooks(opts = {}) {
|
|
322
|
+
const cwd = process.cwd();
|
|
323
|
+
const gitPath = join7(cwd, ".git");
|
|
324
|
+
if (!existsSync6(gitPath)) {
|
|
325
|
+
if (opts.fromInit) {
|
|
326
|
+
log.warn("pre-commit gate not armed: not a git repository yet. Run `cdd-kit install-hooks` after `git init`.");
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
log.error("not a git repository (no .git/ found in cwd)");
|
|
330
|
+
process.exit(1);
|
|
331
|
+
}
|
|
332
|
+
const hooksDir = resolveHooksDir(cwd, gitPath, opts.fromInit ?? false);
|
|
333
|
+
if (hooksDir === null)
|
|
334
|
+
return;
|
|
335
|
+
mkdirSync4(hooksDir, { recursive: true });
|
|
336
|
+
const dest = join7(hooksDir, "pre-commit");
|
|
337
|
+
const ourHook = readFileSync5(join7(ASSET.hooks, "pre-commit"), "utf8");
|
|
338
|
+
let final;
|
|
339
|
+
if (!existsSync6(dest)) {
|
|
340
|
+
final = ourHook;
|
|
341
|
+
} else {
|
|
342
|
+
const existing = readFileSync5(dest, "utf8");
|
|
343
|
+
const startIdx = existing.indexOf(START_MARKER2);
|
|
344
|
+
const endIdx = existing.indexOf(END_MARKER2);
|
|
345
|
+
if (startIdx >= 0 && endIdx > startIdx) {
|
|
346
|
+
const before = existing.slice(0, startIdx);
|
|
347
|
+
const after = existing.slice(endIdx + END_MARKER2.length);
|
|
348
|
+
const ourStart = ourHook.indexOf(START_MARKER2);
|
|
349
|
+
const ourEnd = ourHook.indexOf(END_MARKER2) + END_MARKER2.length;
|
|
350
|
+
const ourBlock = ourHook.slice(ourStart, ourEnd);
|
|
351
|
+
final = before + ourBlock + after;
|
|
352
|
+
} else {
|
|
353
|
+
const ourStart = ourHook.indexOf(START_MARKER2);
|
|
354
|
+
const ourEnd = ourHook.indexOf(END_MARKER2) + END_MARKER2.length;
|
|
355
|
+
const ourBlock = ourHook.slice(ourStart, ourEnd);
|
|
356
|
+
if (existing.startsWith("#!")) {
|
|
357
|
+
const firstNewline = existing.indexOf("\n");
|
|
358
|
+
const shebang = existing.slice(0, firstNewline + 1);
|
|
359
|
+
const rest = existing.slice(firstNewline + 1);
|
|
360
|
+
final = shebang + "\n" + ourBlock + "\n" + rest;
|
|
361
|
+
} else {
|
|
362
|
+
final = "#!/bin/sh\n" + ourBlock + "\n" + existing;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
writeFileSync4(dest, final, "utf8");
|
|
367
|
+
try {
|
|
368
|
+
chmodSync3(dest, 493);
|
|
369
|
+
} catch {
|
|
370
|
+
}
|
|
371
|
+
log.ok(`pre-commit hook installed at ${dest}`);
|
|
372
|
+
log.info("cdd-kit gate will now run automatically before each commit affecting specs/changes/");
|
|
373
|
+
}
|
|
374
|
+
var START_MARKER2, END_MARKER2;
|
|
375
|
+
var init_install_hooks = __esm({
|
|
376
|
+
"src/commands/install-hooks.ts"() {
|
|
377
|
+
"use strict";
|
|
378
|
+
init_paths();
|
|
379
|
+
init_logger();
|
|
380
|
+
START_MARKER2 = "# cdd-kit-managed-block-start";
|
|
381
|
+
END_MARKER2 = "# cdd-kit-managed-block-end";
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// src/utils/provider.ts
|
|
386
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
|
|
387
|
+
import { join as join9 } from "path";
|
|
198
388
|
function validateProviderOption(provider) {
|
|
199
389
|
return provider === "auto" || provider === "claude" || provider === "codex" || provider === "both";
|
|
200
390
|
}
|
|
201
391
|
function inferProvider(cwd, requested = "auto") {
|
|
202
392
|
if (requested !== "auto")
|
|
203
393
|
return requested;
|
|
204
|
-
const modelPolicyPath =
|
|
205
|
-
if (
|
|
394
|
+
const modelPolicyPath = join9(cwd, ".cdd", "model-policy.json");
|
|
395
|
+
if (existsSync8(modelPolicyPath)) {
|
|
206
396
|
try {
|
|
207
|
-
const policy = JSON.parse(
|
|
397
|
+
const policy = JSON.parse(readFileSync7(modelPolicyPath, "utf8"));
|
|
208
398
|
if (policy.provider === "claude" || policy.provider === "codex" || policy.provider === "both") {
|
|
209
399
|
return policy.provider;
|
|
210
400
|
}
|
|
211
401
|
} catch {
|
|
212
402
|
}
|
|
213
403
|
}
|
|
214
|
-
const hasClaude =
|
|
215
|
-
const hasCodex =
|
|
404
|
+
const hasClaude = existsSync8(join9(cwd, "CLAUDE.md")) || existsSync8(join9(cwd, "AGENTS.md"));
|
|
405
|
+
const hasCodex = existsSync8(join9(cwd, "CODEX.md"));
|
|
216
406
|
if (hasClaude && hasCodex)
|
|
217
407
|
return "both";
|
|
218
408
|
if (hasCodex)
|
|
@@ -226,27 +416,27 @@ var init_provider = __esm({
|
|
|
226
416
|
});
|
|
227
417
|
|
|
228
418
|
// src/commands/update.ts
|
|
229
|
-
import { join as
|
|
230
|
-
import { existsSync as
|
|
419
|
+
import { join as join10 } from "path";
|
|
420
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync5, readdirSync as readdirSync3, copyFileSync as copyFileSync3, readFileSync as readFileSync8 } from "fs";
|
|
231
421
|
import { createHash } from "crypto";
|
|
232
422
|
import { homedir as homedir2 } from "os";
|
|
233
423
|
function fileHash(filePath) {
|
|
234
|
-
const buf =
|
|
424
|
+
const buf = readFileSync8(filePath);
|
|
235
425
|
return createHash("sha256").update(buf).digest("hex");
|
|
236
426
|
}
|
|
237
427
|
function diffDir(src, dest) {
|
|
238
428
|
const entries = [];
|
|
239
|
-
if (!
|
|
429
|
+
if (!existsSync9(src))
|
|
240
430
|
return entries;
|
|
241
431
|
function walk(currentSrc, currentDest) {
|
|
242
432
|
const items = readdirSync3(currentSrc, { withFileTypes: true });
|
|
243
433
|
for (const item of items) {
|
|
244
|
-
const srcPath =
|
|
245
|
-
const destPath =
|
|
434
|
+
const srcPath = join10(currentSrc, item.name);
|
|
435
|
+
const destPath = join10(currentDest, item.name);
|
|
246
436
|
if (item.isDirectory()) {
|
|
247
437
|
walk(srcPath, destPath);
|
|
248
438
|
} else {
|
|
249
|
-
if (!
|
|
439
|
+
if (!existsSync9(destPath)) {
|
|
250
440
|
entries.push({ src: srcPath, dest: destPath, action: "add" });
|
|
251
441
|
} else if (fileHash(srcPath) !== fileHash(destPath)) {
|
|
252
442
|
entries.push({ src: srcPath, dest: destPath, action: "overwrite" });
|
|
@@ -264,33 +454,33 @@ function applyDir(entries) {
|
|
|
264
454
|
for (const e of entries) {
|
|
265
455
|
if (e.action === "skip")
|
|
266
456
|
continue;
|
|
267
|
-
|
|
268
|
-
|
|
457
|
+
mkdirSync5(join10(e.dest, ".."), { recursive: true });
|
|
458
|
+
copyFileSync3(e.src, e.dest);
|
|
269
459
|
count += 1;
|
|
270
460
|
}
|
|
271
461
|
return count;
|
|
272
462
|
}
|
|
273
463
|
function backupDir(dir, backupDest) {
|
|
274
|
-
if (!
|
|
464
|
+
if (!existsSync9(dir))
|
|
275
465
|
return;
|
|
276
|
-
|
|
466
|
+
mkdirSync5(backupDest, { recursive: true });
|
|
277
467
|
function walk(src, dst) {
|
|
278
468
|
const items = readdirSync3(src, { withFileTypes: true });
|
|
279
469
|
for (const item of items) {
|
|
280
|
-
const s =
|
|
281
|
-
const d =
|
|
470
|
+
const s = join10(src, item.name);
|
|
471
|
+
const d = join10(dst, item.name);
|
|
282
472
|
if (item.isDirectory()) {
|
|
283
|
-
|
|
473
|
+
mkdirSync5(d, { recursive: true });
|
|
284
474
|
walk(s, d);
|
|
285
475
|
} else
|
|
286
|
-
|
|
476
|
+
copyFileSync3(s, d);
|
|
287
477
|
}
|
|
288
478
|
}
|
|
289
479
|
walk(dir, backupDest);
|
|
290
480
|
}
|
|
291
481
|
async function update(opts) {
|
|
292
482
|
if (opts.postinstall) {
|
|
293
|
-
if (!
|
|
483
|
+
if (!existsSync9(join10(SKILLS_HOME, "contract-driven-delivery"))) {
|
|
294
484
|
return;
|
|
295
485
|
}
|
|
296
486
|
opts.yes = true;
|
|
@@ -308,7 +498,7 @@ async function update(opts) {
|
|
|
308
498
|
const provider = inferProvider(cwd, requestedProvider);
|
|
309
499
|
const updateClaudeAssets = provider === "claude" || provider === "both";
|
|
310
500
|
const agentDiff = updateClaudeAssets ? diffDir(ASSET.agents, AGENTS_HOME) : [];
|
|
311
|
-
const skillDiff = updateClaudeAssets ? readdirSync3(ASSET.skills, { withFileTypes: true }).filter((d) => d.isDirectory()).flatMap((d) => diffDir(
|
|
501
|
+
const skillDiff = updateClaudeAssets ? readdirSync3(ASSET.skills, { withFileTypes: true }).filter((d) => d.isDirectory()).flatMap((d) => diffDir(join10(ASSET.skills, d.name), join10(SKILLS_HOME, d.name))) : [];
|
|
312
502
|
const toWrite = [...agentDiff, ...skillDiff].filter((e) => e.action !== "skip");
|
|
313
503
|
const toAdd = toWrite.filter((e) => e.action === "add");
|
|
314
504
|
const toOver = toWrite.filter((e) => e.action === "overwrite");
|
|
@@ -346,13 +536,13 @@ async function update(opts) {
|
|
|
346
536
|
return;
|
|
347
537
|
}
|
|
348
538
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
349
|
-
const backupRoot =
|
|
539
|
+
const backupRoot = join10(homedir2(), ".claude", ".cdd-kit-backup", timestamp);
|
|
350
540
|
if (!quiet) {
|
|
351
541
|
log.blank();
|
|
352
542
|
log.info(`Backing up to ${backupRoot} \u2026`);
|
|
353
543
|
}
|
|
354
|
-
backupDir(AGENTS_HOME,
|
|
355
|
-
backupDir(SKILLS_HOME,
|
|
544
|
+
backupDir(AGENTS_HOME, join10(backupRoot, "agents"));
|
|
545
|
+
backupDir(SKILLS_HOME, join10(backupRoot, "skills"));
|
|
356
546
|
if (!quiet)
|
|
357
547
|
log.ok(`Backup complete: ${backupRoot}`);
|
|
358
548
|
if (!quiet)
|
|
@@ -393,7 +583,7 @@ var init_update = __esm({
|
|
|
393
583
|
});
|
|
394
584
|
|
|
395
585
|
// src/utils/digest.ts
|
|
396
|
-
import { readFileSync as
|
|
586
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
397
587
|
import { createHash as createHash2 } from "crypto";
|
|
398
588
|
function normalizeContentForHash(buf) {
|
|
399
589
|
if (!buf.includes(13))
|
|
@@ -404,7 +594,7 @@ function normalizeContentForHash(buf) {
|
|
|
404
594
|
function sha256OfFileNormalized(path) {
|
|
405
595
|
let buf;
|
|
406
596
|
try {
|
|
407
|
-
buf =
|
|
597
|
+
buf = readFileSync9(path);
|
|
408
598
|
} catch {
|
|
409
599
|
return "";
|
|
410
600
|
}
|
|
@@ -421,9 +611,9 @@ var context_scan_exports = {};
|
|
|
421
611
|
__export(context_scan_exports, {
|
|
422
612
|
contextScan: () => contextScan
|
|
423
613
|
});
|
|
424
|
-
import { existsSync as
|
|
614
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync10, readdirSync as readdirSync4, writeFileSync as writeFileSync6 } from "fs";
|
|
425
615
|
import { createHash as createHash3 } from "crypto";
|
|
426
|
-
import { basename, dirname as dirname3, join as
|
|
616
|
+
import { basename, dirname as dirname3, join as join11, relative as relative2 } from "path";
|
|
427
617
|
function inputsDigest(paths, cwd) {
|
|
428
618
|
const combined = paths.slice().sort().map((p) => {
|
|
429
619
|
const rel = relative2(cwd, p).replace(/\\/g, "/");
|
|
@@ -436,10 +626,10 @@ function stripGlobSuffix(pattern) {
|
|
|
436
626
|
}
|
|
437
627
|
function getForbiddenPaths(cwd) {
|
|
438
628
|
const forbidden = new Set(DEFAULT_FORBIDDEN);
|
|
439
|
-
const policyPath =
|
|
629
|
+
const policyPath = join11(cwd, ".cdd", "context-policy.json");
|
|
440
630
|
try {
|
|
441
|
-
if (
|
|
442
|
-
const policy = JSON.parse(
|
|
631
|
+
if (existsSync10(policyPath)) {
|
|
632
|
+
const policy = JSON.parse(readFileSync10(policyPath, "utf8"));
|
|
443
633
|
for (const pattern of policy.forbiddenPaths ?? []) {
|
|
444
634
|
forbidden.add(stripGlobSuffix(pattern));
|
|
445
635
|
}
|
|
@@ -468,7 +658,7 @@ function buildTree(dir, cwd, forbidden, stats, prefix = "", depth = 0) {
|
|
|
468
658
|
});
|
|
469
659
|
let output = "";
|
|
470
660
|
const visible = entries.filter((entry) => {
|
|
471
|
-
const relPath = relative2(cwd,
|
|
661
|
+
const relPath = relative2(cwd, join11(dir, entry.name));
|
|
472
662
|
return !isForbidden(relPath, forbidden);
|
|
473
663
|
});
|
|
474
664
|
const truncated = visible.length > PER_DIR_ENTRY_CAP;
|
|
@@ -476,7 +666,7 @@ function buildTree(dir, cwd, forbidden, stats, prefix = "", depth = 0) {
|
|
|
476
666
|
if (truncated)
|
|
477
667
|
stats.truncatedDirs += 1;
|
|
478
668
|
shown.forEach((entry, index2) => {
|
|
479
|
-
const fullPath =
|
|
669
|
+
const fullPath = join11(dir, entry.name);
|
|
480
670
|
const isLast = index2 === shown.length - 1 && !truncated;
|
|
481
671
|
const connector = isLast ? "\\-- " : "|-- ";
|
|
482
672
|
output += `${prefix}${connector}${entry.name}${entry.isDirectory() ? "/" : ""}
|
|
@@ -538,10 +728,10 @@ function parseContractMetadata(content) {
|
|
|
538
728
|
return { title: firstHeading(content), summary, metadata };
|
|
539
729
|
}
|
|
540
730
|
function findContractFiles(dir, found = []) {
|
|
541
|
-
if (!
|
|
731
|
+
if (!existsSync10(dir))
|
|
542
732
|
return found;
|
|
543
733
|
for (const entry of readdirSync4(dir, { withFileTypes: true })) {
|
|
544
|
-
const fullPath =
|
|
734
|
+
const fullPath = join11(dir, entry.name);
|
|
545
735
|
if (entry.isDirectory())
|
|
546
736
|
findContractFiles(fullPath, found);
|
|
547
737
|
else if (entry.isFile() && entry.name.endsWith(".md") && entry.name !== "INDEX.md" && entry.name !== "CHANGELOG.md")
|
|
@@ -551,14 +741,14 @@ function findContractFiles(dir, found = []) {
|
|
|
551
741
|
}
|
|
552
742
|
async function contextScan(opts = {}) {
|
|
553
743
|
const cwd = process.cwd();
|
|
554
|
-
const specsContextDir =
|
|
555
|
-
|
|
744
|
+
const specsContextDir = join11(cwd, "specs", "context");
|
|
745
|
+
mkdirSync6(specsContextDir, { recursive: true });
|
|
556
746
|
const forbidden = getForbiddenPaths(cwd);
|
|
557
747
|
const surface = opts.surface;
|
|
558
748
|
let scanRoot = cwd;
|
|
559
749
|
if (surface) {
|
|
560
|
-
const resolvedSurface =
|
|
561
|
-
if (!
|
|
750
|
+
const resolvedSurface = join11(cwd, surface);
|
|
751
|
+
if (!existsSync10(resolvedSurface)) {
|
|
562
752
|
log.error(`--surface path not found: ${surface}`);
|
|
563
753
|
process.exit(1);
|
|
564
754
|
}
|
|
@@ -570,10 +760,10 @@ async function contextScan(opts = {}) {
|
|
|
570
760
|
}
|
|
571
761
|
const treeStats = { dirs: 0, files: 0, omittedDirs: 0, truncatedDirs: 0 };
|
|
572
762
|
const tree = buildTree(scanRoot, cwd, forbidden, treeStats);
|
|
573
|
-
const policyPath =
|
|
574
|
-
const projectMapInputs = [policyPath].filter(
|
|
575
|
-
|
|
576
|
-
|
|
763
|
+
const policyPath = join11(cwd, ".cdd", "context-policy.json");
|
|
764
|
+
const projectMapInputs = [policyPath].filter(existsSync10);
|
|
765
|
+
writeFileSync6(
|
|
766
|
+
join11(specsContextDir, "project-map.md"),
|
|
577
767
|
[
|
|
578
768
|
"---",
|
|
579
769
|
"artifact: project-map",
|
|
@@ -606,14 +796,14 @@ async function contextScan(opts = {}) {
|
|
|
606
796
|
"utf8"
|
|
607
797
|
);
|
|
608
798
|
log.ok("Created specs/context/project-map.md");
|
|
609
|
-
const contractFiles = findContractFiles(
|
|
799
|
+
const contractFiles = findContractFiles(join11(cwd, "contracts")).sort((a, b) => relative2(cwd, a).localeCompare(relative2(cwd, b)));
|
|
610
800
|
const contractEntries = [];
|
|
611
801
|
const inventoryRows = [];
|
|
612
802
|
let missingSummary = 0;
|
|
613
803
|
for (const file of contractFiles) {
|
|
614
804
|
const relPath = relative2(cwd, file).replace(/\\/g, "/");
|
|
615
805
|
const dir = dirname3(relPath).replace(/\\/g, "/");
|
|
616
|
-
const { title, summary, metadata } = parseContractMetadata(
|
|
806
|
+
const { title, summary, metadata } = parseContractMetadata(readFileSync10(file, "utf8"));
|
|
617
807
|
const contractType = deriveContractType(relPath, metadata);
|
|
618
808
|
const owner = metadata.owner ?? "unknown";
|
|
619
809
|
const surface2 = metadata.surface ?? dir;
|
|
@@ -668,7 +858,7 @@ async function contextScan(opts = {}) {
|
|
|
668
858
|
"",
|
|
669
859
|
...contractEntries
|
|
670
860
|
].join("\n");
|
|
671
|
-
|
|
861
|
+
writeFileSync6(join11(specsContextDir, "contracts-index.md"), contractIndex, "utf8");
|
|
672
862
|
if (missingSummary > 0) {
|
|
673
863
|
log.warn(`Created specs/context/contracts-index.md with ${missingSummary} missing summary warning(s).`);
|
|
674
864
|
} else {
|
|
@@ -3695,7 +3885,7 @@ var require_compile = __commonJS({
|
|
|
3695
3885
|
const schOrFunc = root.refs[ref];
|
|
3696
3886
|
if (schOrFunc)
|
|
3697
3887
|
return schOrFunc;
|
|
3698
|
-
let _sch =
|
|
3888
|
+
let _sch = resolve6.call(this, root, ref);
|
|
3699
3889
|
if (_sch === void 0) {
|
|
3700
3890
|
const schema = (_a = root.localRefs) === null || _a === void 0 ? void 0 : _a[ref];
|
|
3701
3891
|
const { schemaId } = this.opts;
|
|
@@ -3722,7 +3912,7 @@ var require_compile = __commonJS({
|
|
|
3722
3912
|
function sameSchemaEnv(s1, s2) {
|
|
3723
3913
|
return s1.schema === s2.schema && s1.root === s2.root && s1.baseId === s2.baseId;
|
|
3724
3914
|
}
|
|
3725
|
-
function
|
|
3915
|
+
function resolve6(root, ref) {
|
|
3726
3916
|
let sch;
|
|
3727
3917
|
while (typeof (sch = this.refs[ref]) == "string")
|
|
3728
3918
|
ref = sch;
|
|
@@ -4298,7 +4488,7 @@ var require_fast_uri = __commonJS({
|
|
|
4298
4488
|
}
|
|
4299
4489
|
return uri;
|
|
4300
4490
|
}
|
|
4301
|
-
function
|
|
4491
|
+
function resolve6(baseURI, relativeURI, options) {
|
|
4302
4492
|
const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
|
|
4303
4493
|
const resolved = resolveComponent(parse3(baseURI, schemelessOptions), parse3(relativeURI, schemelessOptions), schemelessOptions, true);
|
|
4304
4494
|
schemelessOptions.skipEscape = true;
|
|
@@ -4526,7 +4716,7 @@ var require_fast_uri = __commonJS({
|
|
|
4526
4716
|
var fastUri = {
|
|
4527
4717
|
SCHEMES,
|
|
4528
4718
|
normalize,
|
|
4529
|
-
resolve:
|
|
4719
|
+
resolve: resolve6,
|
|
4530
4720
|
resolveComponent,
|
|
4531
4721
|
equal,
|
|
4532
4722
|
serialize,
|
|
@@ -7515,18 +7705,265 @@ var require_dist = __commonJS({
|
|
|
7515
7705
|
}
|
|
7516
7706
|
});
|
|
7517
7707
|
|
|
7708
|
+
// src/utils/tier-floor.ts
|
|
7709
|
+
import { existsSync as existsSync13, readFileSync as readFileSync12 } from "fs";
|
|
7710
|
+
import { join as join14 } from "path";
|
|
7711
|
+
function loadTierPolicy(cwd) {
|
|
7712
|
+
const path = join14(cwd, ".cdd", "tier-policy.json");
|
|
7713
|
+
if (!existsSync13(path))
|
|
7714
|
+
return DEFAULT_TIER_POLICY;
|
|
7715
|
+
try {
|
|
7716
|
+
const raw = JSON.parse(readFileSync12(path, "utf8"));
|
|
7717
|
+
if (typeof raw.enabled === "boolean" && raw.enabled === false) {
|
|
7718
|
+
return { enabled: false, rules: [] };
|
|
7719
|
+
}
|
|
7720
|
+
if (!Array.isArray(raw.rules))
|
|
7721
|
+
return DEFAULT_TIER_POLICY;
|
|
7722
|
+
const rules = [];
|
|
7723
|
+
for (const r of raw.rules) {
|
|
7724
|
+
if (r && typeof r === "object" && typeof r.maxTier === "number" && Array.isArray(r.patterns)) {
|
|
7725
|
+
const rule = r;
|
|
7726
|
+
rules.push({
|
|
7727
|
+
maxTier: rule.maxTier,
|
|
7728
|
+
label: typeof rule.label === "string" ? rule.label : `tier ${rule.maxTier} surface`,
|
|
7729
|
+
patterns: rule.patterns.filter((p) => typeof p === "string")
|
|
7730
|
+
});
|
|
7731
|
+
}
|
|
7732
|
+
}
|
|
7733
|
+
if (rules.length === 0)
|
|
7734
|
+
return DEFAULT_TIER_POLICY;
|
|
7735
|
+
return { enabled: raw.enabled !== false, rules };
|
|
7736
|
+
} catch {
|
|
7737
|
+
return DEFAULT_TIER_POLICY;
|
|
7738
|
+
}
|
|
7739
|
+
}
|
|
7740
|
+
function patternToRegExp(pattern) {
|
|
7741
|
+
try {
|
|
7742
|
+
return new RegExp(`(?<![a-z0-9])(?:${pattern})(?![a-z0-9])`, "i");
|
|
7743
|
+
} catch {
|
|
7744
|
+
return null;
|
|
7745
|
+
}
|
|
7746
|
+
}
|
|
7747
|
+
function computeTierFloor(text, policy = DEFAULT_TIER_POLICY, opts = {}) {
|
|
7748
|
+
if (!policy.enabled) {
|
|
7749
|
+
return { floorTier: null, label: null, matched: [] };
|
|
7750
|
+
}
|
|
7751
|
+
let floorTier = null;
|
|
7752
|
+
let label = null;
|
|
7753
|
+
const matched = /* @__PURE__ */ new Set();
|
|
7754
|
+
const scan = (rules, haystack) => {
|
|
7755
|
+
if (!haystack)
|
|
7756
|
+
return;
|
|
7757
|
+
for (const rule of rules) {
|
|
7758
|
+
let ruleHit = false;
|
|
7759
|
+
for (const pattern of rule.patterns) {
|
|
7760
|
+
const re = patternToRegExp(pattern);
|
|
7761
|
+
const hit = re ? re.exec(haystack) : null;
|
|
7762
|
+
if (hit) {
|
|
7763
|
+
ruleHit = true;
|
|
7764
|
+
matched.add(hit[0].toLowerCase().replace(/\s+/g, " ").trim());
|
|
7765
|
+
}
|
|
7766
|
+
}
|
|
7767
|
+
if (ruleHit && (floorTier === null || rule.maxTier < floorTier)) {
|
|
7768
|
+
floorTier = rule.maxTier;
|
|
7769
|
+
label = rule.label;
|
|
7770
|
+
}
|
|
7771
|
+
}
|
|
7772
|
+
};
|
|
7773
|
+
scan(policy.rules, text);
|
|
7774
|
+
const paths = opts.paths ?? [];
|
|
7775
|
+
if (paths.length > 0) {
|
|
7776
|
+
scan(policy.rules.filter((r) => r.maxTier === 0), paths.join("\n"));
|
|
7777
|
+
}
|
|
7778
|
+
return { floorTier, label, matched: [...matched].sort() };
|
|
7779
|
+
}
|
|
7780
|
+
var DEFAULT_TIER_POLICY;
|
|
7781
|
+
var init_tier_floor = __esm({
|
|
7782
|
+
"src/utils/tier-floor.ts"() {
|
|
7783
|
+
"use strict";
|
|
7784
|
+
DEFAULT_TIER_POLICY = {
|
|
7785
|
+
enabled: true,
|
|
7786
|
+
rules: [
|
|
7787
|
+
{
|
|
7788
|
+
maxTier: 0,
|
|
7789
|
+
label: "critical surface (auth / payments / data migration / concurrency / secrets)",
|
|
7790
|
+
patterns: [
|
|
7791
|
+
"auth",
|
|
7792
|
+
"authn",
|
|
7793
|
+
"authz",
|
|
7794
|
+
"authentication",
|
|
7795
|
+
"authorization",
|
|
7796
|
+
"authenticate",
|
|
7797
|
+
"authorize",
|
|
7798
|
+
"authenticated",
|
|
7799
|
+
"authorized",
|
|
7800
|
+
"login",
|
|
7801
|
+
"logout",
|
|
7802
|
+
"sign-?in",
|
|
7803
|
+
"sign-?up",
|
|
7804
|
+
"passwords?",
|
|
7805
|
+
"passwd",
|
|
7806
|
+
"credentials?",
|
|
7807
|
+
"secrets?",
|
|
7808
|
+
"api[- ]?keys?",
|
|
7809
|
+
// Qualified so security tokens trip the floor but design/CSS "tokens" do not.
|
|
7810
|
+
"(access|api|auth|session|bearer|refresh|csrf|id|reset)[- ]?tokens?",
|
|
7811
|
+
"jwt",
|
|
7812
|
+
"oauth",
|
|
7813
|
+
"oidc",
|
|
7814
|
+
"saml",
|
|
7815
|
+
"sessions?",
|
|
7816
|
+
"cookies?",
|
|
7817
|
+
"payments?",
|
|
7818
|
+
"billing",
|
|
7819
|
+
"invoices?",
|
|
7820
|
+
"charges?",
|
|
7821
|
+
"refunds?",
|
|
7822
|
+
"checkout",
|
|
7823
|
+
"stripe",
|
|
7824
|
+
"paypal",
|
|
7825
|
+
"migrations?",
|
|
7826
|
+
"migrate",
|
|
7827
|
+
"alter table",
|
|
7828
|
+
"drop table",
|
|
7829
|
+
"drop column",
|
|
7830
|
+
"schema change",
|
|
7831
|
+
"concurrency",
|
|
7832
|
+
"race condition",
|
|
7833
|
+
"mutex",
|
|
7834
|
+
"deadlock",
|
|
7835
|
+
"transaction isolation",
|
|
7836
|
+
"encrypt",
|
|
7837
|
+
"decrypt",
|
|
7838
|
+
"crypto",
|
|
7839
|
+
"hashing",
|
|
7840
|
+
"rbac",
|
|
7841
|
+
"permissions?",
|
|
7842
|
+
"access control",
|
|
7843
|
+
"privileges?",
|
|
7844
|
+
"pii",
|
|
7845
|
+
"gdpr",
|
|
7846
|
+
"hipaa",
|
|
7847
|
+
"rate limit",
|
|
7848
|
+
"csrf",
|
|
7849
|
+
"xss",
|
|
7850
|
+
"sql injection"
|
|
7851
|
+
]
|
|
7852
|
+
},
|
|
7853
|
+
{
|
|
7854
|
+
maxTier: 2,
|
|
7855
|
+
label: "behavioral surface (api / data shape / queue / cache / external integration)",
|
|
7856
|
+
patterns: [
|
|
7857
|
+
"endpoint",
|
|
7858
|
+
"route",
|
|
7859
|
+
"api contract",
|
|
7860
|
+
"request schema",
|
|
7861
|
+
"response schema",
|
|
7862
|
+
"pagination",
|
|
7863
|
+
"queue",
|
|
7864
|
+
"worker",
|
|
7865
|
+
"cron",
|
|
7866
|
+
"scheduler",
|
|
7867
|
+
"webhook",
|
|
7868
|
+
"cache",
|
|
7869
|
+
"redis",
|
|
7870
|
+
"database",
|
|
7871
|
+
"query",
|
|
7872
|
+
"index",
|
|
7873
|
+
"external service",
|
|
7874
|
+
"third[- ]?party",
|
|
7875
|
+
"integration",
|
|
7876
|
+
"data shape",
|
|
7877
|
+
"nullable",
|
|
7878
|
+
"breaking change"
|
|
7879
|
+
]
|
|
7880
|
+
}
|
|
7881
|
+
]
|
|
7882
|
+
};
|
|
7883
|
+
}
|
|
7884
|
+
});
|
|
7885
|
+
|
|
7886
|
+
// src/utils/git-paths.ts
|
|
7887
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
7888
|
+
function unquote(raw) {
|
|
7889
|
+
return raw.trim().replace(/^"(.*)"$/, "$1");
|
|
7890
|
+
}
|
|
7891
|
+
function getTouchedPaths(cwd) {
|
|
7892
|
+
let res;
|
|
7893
|
+
try {
|
|
7894
|
+
res = spawnSync3("git", ["status", "--porcelain", "--untracked-files=all"], { cwd, encoding: "utf8" });
|
|
7895
|
+
} catch {
|
|
7896
|
+
return [];
|
|
7897
|
+
}
|
|
7898
|
+
if (res.status !== 0 || !res.stdout)
|
|
7899
|
+
return [];
|
|
7900
|
+
const paths = [];
|
|
7901
|
+
for (const line of res.stdout.split("\n")) {
|
|
7902
|
+
if (!line.trim())
|
|
7903
|
+
continue;
|
|
7904
|
+
const body = line.slice(3);
|
|
7905
|
+
const arrow = body.indexOf(" -> ");
|
|
7906
|
+
if (arrow >= 0) {
|
|
7907
|
+
for (const part of [body.slice(0, arrow), body.slice(arrow + 4)]) {
|
|
7908
|
+
const clean = unquote(part);
|
|
7909
|
+
if (clean)
|
|
7910
|
+
paths.push(clean);
|
|
7911
|
+
}
|
|
7912
|
+
} else {
|
|
7913
|
+
const clean = unquote(body);
|
|
7914
|
+
if (clean)
|
|
7915
|
+
paths.push(clean);
|
|
7916
|
+
}
|
|
7917
|
+
}
|
|
7918
|
+
return paths;
|
|
7919
|
+
}
|
|
7920
|
+
function getStagedPaths(cwd) {
|
|
7921
|
+
let res;
|
|
7922
|
+
try {
|
|
7923
|
+
res = spawnSync3("git", ["diff", "--cached", "--name-status", "--no-color"], { cwd, encoding: "utf8" });
|
|
7924
|
+
} catch {
|
|
7925
|
+
return [];
|
|
7926
|
+
}
|
|
7927
|
+
if (res.status !== 0 || !res.stdout)
|
|
7928
|
+
return [];
|
|
7929
|
+
const paths = [];
|
|
7930
|
+
for (const line of res.stdout.split("\n")) {
|
|
7931
|
+
if (!line.trim())
|
|
7932
|
+
continue;
|
|
7933
|
+
const parts = line.split(" ");
|
|
7934
|
+
const status = parts[0] ?? "";
|
|
7935
|
+
if (/^[RC]/.test(status) && parts.length >= 3) {
|
|
7936
|
+
for (const part of [parts[1], parts[2]]) {
|
|
7937
|
+
const clean = unquote(part ?? "");
|
|
7938
|
+
if (clean)
|
|
7939
|
+
paths.push(clean);
|
|
7940
|
+
}
|
|
7941
|
+
} else if (parts[1]) {
|
|
7942
|
+
const clean = unquote(parts[1]);
|
|
7943
|
+
if (clean)
|
|
7944
|
+
paths.push(clean);
|
|
7945
|
+
}
|
|
7946
|
+
}
|
|
7947
|
+
return paths;
|
|
7948
|
+
}
|
|
7949
|
+
var init_git_paths = __esm({
|
|
7950
|
+
"src/utils/git-paths.ts"() {
|
|
7951
|
+
"use strict";
|
|
7952
|
+
}
|
|
7953
|
+
});
|
|
7954
|
+
|
|
7518
7955
|
// src/utils/gitignore.ts
|
|
7519
|
-
import { existsSync as
|
|
7520
|
-
import { join as
|
|
7956
|
+
import { existsSync as existsSync16, readFileSync as readFileSync15, writeFileSync as writeFileSync9 } from "fs";
|
|
7957
|
+
import { join as join16 } from "path";
|
|
7521
7958
|
function ensureGitignoreEntry(cwd, entry, comment = "cdd-kit generated backups (do not commit)") {
|
|
7522
|
-
const path =
|
|
7959
|
+
const path = join16(cwd, ".gitignore");
|
|
7523
7960
|
const trimmed = entry.trim();
|
|
7524
7961
|
if (!trimmed)
|
|
7525
7962
|
return false;
|
|
7526
7963
|
const re = new RegExp(`^\\s*${trimmed.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*$`, "m");
|
|
7527
7964
|
let existing = "";
|
|
7528
|
-
if (
|
|
7529
|
-
existing =
|
|
7965
|
+
if (existsSync16(path))
|
|
7966
|
+
existing = readFileSync15(path, "utf8");
|
|
7530
7967
|
if (re.test(existing))
|
|
7531
7968
|
return false;
|
|
7532
7969
|
const sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
@@ -7536,7 +7973,7 @@ ${trimmed}
|
|
|
7536
7973
|
# ${comment}
|
|
7537
7974
|
${trimmed}
|
|
7538
7975
|
`;
|
|
7539
|
-
|
|
7976
|
+
writeFileSync9(path, existing + block, "utf8");
|
|
7540
7977
|
return true;
|
|
7541
7978
|
}
|
|
7542
7979
|
var init_gitignore = __esm({
|
|
@@ -7550,15 +7987,15 @@ var migrate_exports = {};
|
|
|
7550
7987
|
__export(migrate_exports, {
|
|
7551
7988
|
migrate: () => migrate
|
|
7552
7989
|
});
|
|
7553
|
-
import { join as
|
|
7554
|
-
import { cpSync as cpSync2, existsSync as
|
|
7990
|
+
import { join as join17 } from "path";
|
|
7991
|
+
import { cpSync as cpSync2, existsSync as existsSync17, mkdirSync as mkdirSync8, readdirSync as readdirSync7, readFileSync as readFileSync16, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync10 } from "fs";
|
|
7555
7992
|
import yaml2 from "js-yaml";
|
|
7556
7993
|
function backupChangeDir(cwd, changeId, sessionStamp) {
|
|
7557
|
-
const backupRoot =
|
|
7558
|
-
const backupDir2 =
|
|
7559
|
-
|
|
7560
|
-
const sourceDir =
|
|
7561
|
-
if (
|
|
7994
|
+
const backupRoot = join17(cwd, ".cdd", "migrate-backup", sessionStamp);
|
|
7995
|
+
const backupDir2 = join17(backupRoot, changeId);
|
|
7996
|
+
mkdirSync8(backupRoot, { recursive: true });
|
|
7997
|
+
const sourceDir = join17(cwd, "specs", "changes", changeId);
|
|
7998
|
+
if (existsSync17(sourceDir)) {
|
|
7562
7999
|
cpSync2(sourceDir, backupDir2, { recursive: true });
|
|
7563
8000
|
}
|
|
7564
8001
|
return backupDir2;
|
|
@@ -7691,16 +8128,16 @@ function parseLegacyTaskList(body) {
|
|
|
7691
8128
|
return rows;
|
|
7692
8129
|
}
|
|
7693
8130
|
function migrateTasksFile(changeId, changeDir, enableContextGovernance, detectedTier, changed, warnings, pendingWrites, pendingDeletes) {
|
|
7694
|
-
const newPath =
|
|
7695
|
-
const legacyPath =
|
|
7696
|
-
if (
|
|
8131
|
+
const newPath = join17(changeDir, "tasks.yml");
|
|
8132
|
+
const legacyPath = join17(changeDir, "tasks.md");
|
|
8133
|
+
if (existsSync17(newPath)) {
|
|
7697
8134
|
return;
|
|
7698
8135
|
}
|
|
7699
|
-
if (!
|
|
8136
|
+
if (!existsSync17(legacyPath)) {
|
|
7700
8137
|
warnings.push("tasks.md not found and tasks.yml missing \u2014 skipping tasks migration");
|
|
7701
8138
|
return;
|
|
7702
8139
|
}
|
|
7703
|
-
const raw =
|
|
8140
|
+
const raw = readFileSync16(legacyPath, "utf8");
|
|
7704
8141
|
const fm = parseLegacyFrontmatter(raw);
|
|
7705
8142
|
const bodyMatch = raw.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?([\s\S]*)$/);
|
|
7706
8143
|
const body = bodyMatch ? bodyMatch[1] : raw;
|
|
@@ -7784,17 +8221,17 @@ function parseLegacyAgentLog(content) {
|
|
|
7784
8221
|
return data;
|
|
7785
8222
|
}
|
|
7786
8223
|
function migrateAgentLogs(changeDir, changed, pendingWrites, pendingDeletes) {
|
|
7787
|
-
const agentLogDir =
|
|
7788
|
-
if (!
|
|
8224
|
+
const agentLogDir = join17(changeDir, "agent-log");
|
|
8225
|
+
if (!existsSync17(agentLogDir))
|
|
7789
8226
|
return;
|
|
7790
8227
|
const mdLogs = readdirSync7(agentLogDir).filter((f) => f.endsWith(".md"));
|
|
7791
8228
|
for (const f of mdLogs) {
|
|
7792
|
-
const fullPath =
|
|
8229
|
+
const fullPath = join17(agentLogDir, f);
|
|
7793
8230
|
const yamlName = f.replace(/\.md$/, ".yml");
|
|
7794
|
-
const yamlFull =
|
|
7795
|
-
if (
|
|
8231
|
+
const yamlFull = join17(agentLogDir, yamlName);
|
|
8232
|
+
if (existsSync17(yamlFull))
|
|
7796
8233
|
continue;
|
|
7797
|
-
const raw =
|
|
8234
|
+
const raw = readFileSync16(fullPath, "utf8");
|
|
7798
8235
|
const parsed = parseLegacyAgentLog(raw);
|
|
7799
8236
|
const yamlOut = yaml2.dump(parsed, { lineWidth: -1, noRefs: true });
|
|
7800
8237
|
pendingWrites.push({ path: yamlFull, content: yamlOut });
|
|
@@ -7803,15 +8240,15 @@ function migrateAgentLogs(changeDir, changed, pendingWrites, pendingDeletes) {
|
|
|
7803
8240
|
}
|
|
7804
8241
|
}
|
|
7805
8242
|
function ensureImplementationPlanScaffold(changeId, changeDir, changed, warnings, pendingWrites) {
|
|
7806
|
-
const planPath =
|
|
7807
|
-
if (
|
|
8243
|
+
const planPath = join17(changeDir, "implementation-plan.md");
|
|
8244
|
+
if (existsSync17(planPath))
|
|
7808
8245
|
return;
|
|
7809
|
-
const templatePath =
|
|
7810
|
-
if (!
|
|
8246
|
+
const templatePath = join17(ASSET.specsTemplates, "implementation-plan.md");
|
|
8247
|
+
if (!existsSync17(templatePath)) {
|
|
7811
8248
|
warnings.push("implementation-plan.md template not found; run cdd-kit upgrade --yes after updating cdd-kit");
|
|
7812
8249
|
return;
|
|
7813
8250
|
}
|
|
7814
|
-
const template =
|
|
8251
|
+
const template = readFileSync16(templatePath, "utf8").replace(/<change-id>/g, changeId).replace(/<id>/g, changeId);
|
|
7815
8252
|
pendingWrites.push({ path: planPath, content: template });
|
|
7816
8253
|
changed.push("implementation-plan.md: added scaffold");
|
|
7817
8254
|
warnings.push("implementation-plan.md scaffold added; fill it before implementation agents continue");
|
|
@@ -7822,9 +8259,9 @@ function migrateOne(changeId, changeDir, enableContextGovernance) {
|
|
|
7822
8259
|
const pending = [];
|
|
7823
8260
|
const deletes = [];
|
|
7824
8261
|
let detectedTier = null;
|
|
7825
|
-
const classifPath =
|
|
7826
|
-
if (
|
|
7827
|
-
const content =
|
|
8262
|
+
const classifPath = join17(changeDir, "change-classification.md");
|
|
8263
|
+
if (existsSync17(classifPath)) {
|
|
8264
|
+
const content = readFileSync16(classifPath, "utf8");
|
|
7828
8265
|
const hasNewTierFormat = /^## Tier\s*\n\s*-\s*\d\s*$/m.test(content);
|
|
7829
8266
|
const oldMatch = content.match(/\*\*Tier[:\*]+\s*(?:Tier\s*)?(\d)/i) ?? content.match(/^-?\s*Tier:\s*(?:Tier\s*)?(\d)/mi);
|
|
7830
8267
|
if (oldMatch)
|
|
@@ -7855,8 +8292,8 @@ function migrateOne(changeId, changeDir, enableContextGovernance) {
|
|
|
7855
8292
|
migrateTasksFile(changeId, changeDir, enableContextGovernance, detectedTier, changed, warnings, pending, deletes);
|
|
7856
8293
|
ensureImplementationPlanScaffold(changeId, changeDir, changed, warnings, pending);
|
|
7857
8294
|
migrateAgentLogs(changeDir, changed, pending, deletes);
|
|
7858
|
-
const manifestPath =
|
|
7859
|
-
if (!
|
|
8295
|
+
const manifestPath = join17(changeDir, "context-manifest.md");
|
|
8296
|
+
if (!existsSync17(manifestPath)) {
|
|
7860
8297
|
changed.push(enableContextGovernance ? "context-manifest.md: added context-governance v1 manifest scaffold" : "context-manifest.md: added legacy context manifest scaffold");
|
|
7861
8298
|
pending.push({
|
|
7862
8299
|
path: manifestPath,
|
|
@@ -7872,7 +8309,7 @@ function commitWritesAtomically(pending, deletes) {
|
|
|
7872
8309
|
try {
|
|
7873
8310
|
for (const write of pending) {
|
|
7874
8311
|
const tmp = `${write.path}.cdd-migrate.tmp`;
|
|
7875
|
-
|
|
8312
|
+
writeFileSync10(tmp, write.content, "utf8");
|
|
7876
8313
|
renames.push({ tmp, final: write.path });
|
|
7877
8314
|
}
|
|
7878
8315
|
} catch (err) {
|
|
@@ -7901,8 +8338,8 @@ async function migrate(changeId, opts = {}) {
|
|
|
7901
8338
|
const noBackup = opts.noBackup ?? false;
|
|
7902
8339
|
const idsToMigrate = [];
|
|
7903
8340
|
if (opts.all) {
|
|
7904
|
-
const changesDir =
|
|
7905
|
-
if (!
|
|
8341
|
+
const changesDir = join17(cwd, "specs", "changes");
|
|
8342
|
+
if (!existsSync17(changesDir)) {
|
|
7906
8343
|
log.info("No specs/changes/ directory found \u2014 nothing to migrate.");
|
|
7907
8344
|
return;
|
|
7908
8345
|
}
|
|
@@ -7910,8 +8347,8 @@ async function migrate(changeId, opts = {}) {
|
|
|
7910
8347
|
...readdirSync7(changesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name)
|
|
7911
8348
|
);
|
|
7912
8349
|
} else if (changeId) {
|
|
7913
|
-
const specificDir =
|
|
7914
|
-
if (!
|
|
8350
|
+
const specificDir = join17(cwd, "specs", "changes", changeId);
|
|
8351
|
+
if (!existsSync17(specificDir)) {
|
|
7915
8352
|
log.error(`Change not found: specs/changes/${changeId}`);
|
|
7916
8353
|
process.exit(1);
|
|
7917
8354
|
}
|
|
@@ -7931,10 +8368,10 @@ async function migrate(changeId, opts = {}) {
|
|
|
7931
8368
|
const sessionStamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
7932
8369
|
let migratedCount = 0;
|
|
7933
8370
|
let upToDateCount = 0;
|
|
7934
|
-
const backupRoot =
|
|
8371
|
+
const backupRoot = join17(cwd, ".cdd", "migrate-backup", sessionStamp);
|
|
7935
8372
|
for (const id of idsToMigrate) {
|
|
7936
|
-
const changeDir =
|
|
7937
|
-
if (!
|
|
8373
|
+
const changeDir = join17(cwd, "specs", "changes", id);
|
|
8374
|
+
if (!existsSync17(changeDir)) {
|
|
7938
8375
|
log.warn(` ${id}: directory not found \u2014 skipping`);
|
|
7939
8376
|
continue;
|
|
7940
8377
|
}
|
|
@@ -7996,40 +8433,40 @@ var upgrade_exports = {};
|
|
|
7996
8433
|
__export(upgrade_exports, {
|
|
7997
8434
|
upgrade: () => upgrade
|
|
7998
8435
|
});
|
|
7999
|
-
import { existsSync as
|
|
8000
|
-
import { dirname as
|
|
8436
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync9, readdirSync as readdirSync8, copyFileSync as copyFileSync4, readFileSync as readFileSync17, writeFileSync as writeFileSync11 } from "fs";
|
|
8437
|
+
import { dirname as dirname5, join as join18, relative as relative4 } from "path";
|
|
8001
8438
|
function planMissingFiles(srcDir, destDir, label, planned) {
|
|
8002
|
-
if (!
|
|
8439
|
+
if (!existsSync18(srcDir))
|
|
8003
8440
|
return;
|
|
8004
8441
|
for (const entry of readdirSync8(srcDir, { withFileTypes: true })) {
|
|
8005
|
-
const src =
|
|
8006
|
-
const dest =
|
|
8442
|
+
const src = join18(srcDir, entry.name);
|
|
8443
|
+
const dest = join18(destDir, entry.name);
|
|
8007
8444
|
if (entry.isDirectory()) {
|
|
8008
|
-
planMissingFiles(src, dest,
|
|
8445
|
+
planMissingFiles(src, dest, join18(label, entry.name), planned);
|
|
8009
8446
|
continue;
|
|
8010
8447
|
}
|
|
8011
|
-
if (!
|
|
8012
|
-
planned.push({ src, dest, rel:
|
|
8448
|
+
if (!existsSync18(dest)) {
|
|
8449
|
+
planned.push({ src, dest, rel: join18(label, relative4(srcDir, src)) });
|
|
8013
8450
|
}
|
|
8014
8451
|
}
|
|
8015
8452
|
}
|
|
8016
8453
|
function planProviderGuidance(cwd, provider, planned) {
|
|
8017
8454
|
if (provider === "claude" || provider === "both") {
|
|
8018
|
-
if (!
|
|
8019
|
-
planned.push({ src: ASSET.claudeTemplate, dest:
|
|
8455
|
+
if (!existsSync18(join18(cwd, "CLAUDE.md"))) {
|
|
8456
|
+
planned.push({ src: ASSET.claudeTemplate, dest: join18(cwd, "CLAUDE.md"), rel: "CLAUDE.md" });
|
|
8020
8457
|
}
|
|
8021
|
-
if (!
|
|
8022
|
-
planned.push({ src: ASSET.agentsTemplate, dest:
|
|
8458
|
+
if (!existsSync18(join18(cwd, "AGENTS.md"))) {
|
|
8459
|
+
planned.push({ src: ASSET.agentsTemplate, dest: join18(cwd, "AGENTS.md"), rel: "AGENTS.md" });
|
|
8023
8460
|
}
|
|
8024
8461
|
}
|
|
8025
|
-
if ((provider === "codex" || provider === "both") && !
|
|
8026
|
-
planned.push({ src: ASSET.codexTemplate, dest:
|
|
8462
|
+
if ((provider === "codex" || provider === "both") && !existsSync18(join18(cwd, "CODEX.md"))) {
|
|
8463
|
+
planned.push({ src: ASSET.codexTemplate, dest: join18(cwd, "CODEX.md"), rel: "CODEX.md" });
|
|
8027
8464
|
}
|
|
8028
8465
|
}
|
|
8029
8466
|
function applyCopy(plan) {
|
|
8030
8467
|
for (const item of plan) {
|
|
8031
|
-
|
|
8032
|
-
|
|
8468
|
+
mkdirSync9(dirname5(item.dest), { recursive: true });
|
|
8469
|
+
copyFileSync4(item.src, item.dest);
|
|
8033
8470
|
}
|
|
8034
8471
|
}
|
|
8035
8472
|
async function upgrade(opts = {}) {
|
|
@@ -8041,12 +8478,12 @@ async function upgrade(opts = {}) {
|
|
|
8041
8478
|
}
|
|
8042
8479
|
const provider = inferProvider(cwd, requestedProvider);
|
|
8043
8480
|
const plan = [];
|
|
8044
|
-
planMissingFiles(ASSET.contracts,
|
|
8045
|
-
planMissingFiles(ASSET.specsTemplates,
|
|
8046
|
-
planMissingFiles(ASSET.testsTemplates,
|
|
8047
|
-
planMissingFiles(ASSET.ci,
|
|
8048
|
-
planMissingFiles(ASSET.githubWorkflows,
|
|
8049
|
-
planMissingFiles(ASSET.cddConfig,
|
|
8481
|
+
planMissingFiles(ASSET.contracts, join18(cwd, "contracts"), "contracts", plan);
|
|
8482
|
+
planMissingFiles(ASSET.specsTemplates, join18(cwd, "specs", "templates"), "specs/templates", plan);
|
|
8483
|
+
planMissingFiles(ASSET.testsTemplates, join18(cwd, "tests", "templates"), "tests/templates", plan);
|
|
8484
|
+
planMissingFiles(ASSET.ci, join18(cwd, "ci"), "ci", plan);
|
|
8485
|
+
planMissingFiles(ASSET.githubWorkflows, join18(cwd, ".github", "workflows"), ".github/workflows", plan);
|
|
8486
|
+
planMissingFiles(ASSET.cddConfig, join18(cwd, ".cdd"), ".cdd", plan);
|
|
8050
8487
|
planProviderGuidance(cwd, provider, plan);
|
|
8051
8488
|
log.blank();
|
|
8052
8489
|
log.info(`Upgrade provider: ${provider}`);
|
|
@@ -8083,11 +8520,11 @@ async function upgrade(opts = {}) {
|
|
|
8083
8520
|
return;
|
|
8084
8521
|
}
|
|
8085
8522
|
applyCopy(plan);
|
|
8086
|
-
const modelPolicyPath =
|
|
8087
|
-
if (
|
|
8523
|
+
const modelPolicyPath = join18(cwd, ".cdd", "model-policy.json");
|
|
8524
|
+
if (existsSync18(modelPolicyPath)) {
|
|
8088
8525
|
let existing = {};
|
|
8089
8526
|
try {
|
|
8090
|
-
existing = JSON.parse(
|
|
8527
|
+
existing = JSON.parse(readFileSync17(modelPolicyPath, "utf8"));
|
|
8091
8528
|
} catch {
|
|
8092
8529
|
}
|
|
8093
8530
|
const merged = {
|
|
@@ -8096,7 +8533,7 @@ async function upgrade(opts = {}) {
|
|
|
8096
8533
|
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8097
8534
|
roles: existing.roles && typeof existing.roles === "object" ? existing.roles : {}
|
|
8098
8535
|
};
|
|
8099
|
-
|
|
8536
|
+
writeFileSync11(modelPolicyPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
8100
8537
|
}
|
|
8101
8538
|
log.blank();
|
|
8102
8539
|
log.ok(`Upgrade complete: ${plan.length} missing file(s) added.`);
|
|
@@ -8234,7 +8671,7 @@ function renderYaml(entries, opts) {
|
|
|
8234
8671
|
}
|
|
8235
8672
|
}
|
|
8236
8673
|
}
|
|
8237
|
-
const headerLineCount = opts.sourcesDigest ?
|
|
8674
|
+
const headerLineCount = 2 + (opts.sourcesDigest ? 1 : 0) + (opts.surfaceRoot ? 1 : 0);
|
|
8238
8675
|
const mapLines = bodyLines.length + headerLineCount;
|
|
8239
8676
|
const compression = totalSrc === 0 ? 0 : totalSrc / mapLines;
|
|
8240
8677
|
const fileCount = entries.length;
|
|
@@ -8245,6 +8682,9 @@ function renderYaml(entries, opts) {
|
|
|
8245
8682
|
if (opts.sourcesDigest) {
|
|
8246
8683
|
header.push(`# sources-digest: ${opts.sourcesDigest}`);
|
|
8247
8684
|
}
|
|
8685
|
+
if (opts.surfaceRoot) {
|
|
8686
|
+
header.push(`# surface-root: ${opts.surfaceRoot}`);
|
|
8687
|
+
}
|
|
8248
8688
|
return [...header, ...bodyLines].join("\n") + "\n";
|
|
8249
8689
|
}
|
|
8250
8690
|
var YAML_RESERVED;
|
|
@@ -8256,8 +8696,8 @@ var init_yaml_writer = __esm({
|
|
|
8256
8696
|
});
|
|
8257
8697
|
|
|
8258
8698
|
// src/code-map/config.ts
|
|
8259
|
-
import { existsSync as
|
|
8260
|
-
import { join as
|
|
8699
|
+
import { existsSync as existsSync19, readFileSync as readFileSync18 } from "fs";
|
|
8700
|
+
import { join as join19 } from "path";
|
|
8261
8701
|
import { load as yamlLoad } from "js-yaml";
|
|
8262
8702
|
function asStringArray(value, key, where) {
|
|
8263
8703
|
if (value === void 0)
|
|
@@ -8273,8 +8713,8 @@ function asStringArray(value, key, where) {
|
|
|
8273
8713
|
return value;
|
|
8274
8714
|
}
|
|
8275
8715
|
function loadCodeMapConfig(cwd) {
|
|
8276
|
-
const filePath =
|
|
8277
|
-
if (!
|
|
8716
|
+
const filePath = join19(cwd, CONFIG_REL_PATH);
|
|
8717
|
+
if (!existsSync19(filePath)) {
|
|
8278
8718
|
return {
|
|
8279
8719
|
include: [...BUILTIN_INCLUDE],
|
|
8280
8720
|
exclude: [...BUILTIN_EXCLUDE],
|
|
@@ -8283,7 +8723,7 @@ function loadCodeMapConfig(cwd) {
|
|
|
8283
8723
|
}
|
|
8284
8724
|
let text;
|
|
8285
8725
|
try {
|
|
8286
|
-
text =
|
|
8726
|
+
text = readFileSync18(filePath, "utf8");
|
|
8287
8727
|
} catch (err) {
|
|
8288
8728
|
throw new Error(`failed to read ${CONFIG_REL_PATH}: ${err.message}`);
|
|
8289
8729
|
}
|
|
@@ -8353,8 +8793,8 @@ var init_config = __esm({
|
|
|
8353
8793
|
});
|
|
8354
8794
|
|
|
8355
8795
|
// src/code-map/include-exclude.ts
|
|
8356
|
-
import { readdirSync as readdirSync9, statSync as
|
|
8357
|
-
import { join as
|
|
8796
|
+
import { readdirSync as readdirSync9, statSync as statSync3 } from "fs";
|
|
8797
|
+
import { join as join20 } from "path";
|
|
8358
8798
|
import picomatch from "picomatch";
|
|
8359
8799
|
function walkRepo(root, opts = {}) {
|
|
8360
8800
|
const includes = opts.include ?? [];
|
|
@@ -8373,13 +8813,13 @@ function walkRepo(root, opts = {}) {
|
|
|
8373
8813
|
}
|
|
8374
8814
|
for (const entry of entries) {
|
|
8375
8815
|
const relPath = relDir ? `${relDir}/${entry.name}` : entry.name;
|
|
8376
|
-
const absPath =
|
|
8816
|
+
const absPath = join20(dir, entry.name);
|
|
8377
8817
|
if (entry.isDirectory() || entry.isSymbolicLink()) {
|
|
8378
8818
|
const dirPattern = `${relPath}/**`;
|
|
8379
8819
|
if (isExcluded(relPath) || isExcluded(dirPattern))
|
|
8380
8820
|
continue;
|
|
8381
8821
|
try {
|
|
8382
|
-
const stat =
|
|
8822
|
+
const stat = statSync3(absPath);
|
|
8383
8823
|
if (stat.isDirectory()) {
|
|
8384
8824
|
walk(absPath, relPath);
|
|
8385
8825
|
}
|
|
@@ -8445,10 +8885,10 @@ var init_orchestrator = __esm({
|
|
|
8445
8885
|
});
|
|
8446
8886
|
|
|
8447
8887
|
// src/code-map/freshness.ts
|
|
8448
|
-
import { existsSync as
|
|
8449
|
-
import { join as
|
|
8888
|
+
import { existsSync as existsSync20, readFileSync as readFileSync19, statSync as statSync4 } from "fs";
|
|
8889
|
+
import { join as join21 } from "path";
|
|
8450
8890
|
function checkCodeMapFreshness(cwd, mapRel = ".cdd/code-map.yml", include, exclude) {
|
|
8451
|
-
const mapPath =
|
|
8891
|
+
const mapPath = join21(cwd, mapRel);
|
|
8452
8892
|
let cfg;
|
|
8453
8893
|
try {
|
|
8454
8894
|
cfg = loadCodeMapConfig(cwd);
|
|
@@ -8464,17 +8904,17 @@ function checkCodeMapFreshness(cwd, mapRel = ".cdd/code-map.yml", include, exclu
|
|
|
8464
8904
|
const includeFinal = [...cfg.include, ...include ?? []];
|
|
8465
8905
|
const excludeFinal = [...cfg.exclude, ...exclude ?? []];
|
|
8466
8906
|
const sourceFiles = walkRepo(cwd, { include: includeFinal, exclude: excludeFinal });
|
|
8467
|
-
if (!
|
|
8907
|
+
if (!existsSync20(mapPath)) {
|
|
8468
8908
|
if (sourceFiles.length === 0) {
|
|
8469
8909
|
return { status: "missing-greenfield", staleFiles: [], staleCount: 0, mapPath };
|
|
8470
8910
|
}
|
|
8471
8911
|
return { status: "missing-with-sources", staleFiles: [], staleCount: 0, mapPath };
|
|
8472
8912
|
}
|
|
8473
|
-
const mapMtime =
|
|
8913
|
+
const mapMtime = statSync4(mapPath).mtimeMs;
|
|
8474
8914
|
const staleAll = [];
|
|
8475
8915
|
for (const absPath of sourceFiles) {
|
|
8476
8916
|
try {
|
|
8477
|
-
const mtime =
|
|
8917
|
+
const mtime = statSync4(absPath).mtimeMs;
|
|
8478
8918
|
if (mtime > mapMtime) {
|
|
8479
8919
|
const rel = absPath.replace(/\\/g, "/").replace(cwd.replace(/\\/g, "/") + "/", "");
|
|
8480
8920
|
staleAll.push(rel);
|
|
@@ -8501,7 +8941,7 @@ function checkCodeMapFreshness(cwd, mapRel = ".cdd/code-map.yml", include, exclu
|
|
|
8501
8941
|
}
|
|
8502
8942
|
function readSourcesDigest(mapPath) {
|
|
8503
8943
|
try {
|
|
8504
|
-
const head =
|
|
8944
|
+
const head = readFileSync19(mapPath, "utf8").slice(0, 2048);
|
|
8505
8945
|
const m = head.match(/^# sources-digest:\s*([a-f0-9]+)/m);
|
|
8506
8946
|
return m ? m[1] : null;
|
|
8507
8947
|
} catch {
|
|
@@ -8518,7 +8958,7 @@ var init_freshness = __esm({
|
|
|
8518
8958
|
});
|
|
8519
8959
|
|
|
8520
8960
|
// src/code-map/index-reader.ts
|
|
8521
|
-
import { existsSync as
|
|
8961
|
+
import { existsSync as existsSync21, readFileSync as readFileSync20 } from "fs";
|
|
8522
8962
|
import yaml3 from "js-yaml";
|
|
8523
8963
|
async function ensureCodeMapFresh(mapPath, refresh2) {
|
|
8524
8964
|
if (!refresh2)
|
|
@@ -8556,13 +8996,13 @@ function sidecarPathFor(mapPath) {
|
|
|
8556
8996
|
}
|
|
8557
8997
|
function tryLoadSidecar(mapPath, mapText) {
|
|
8558
8998
|
const sidecarPath = sidecarPathFor(mapPath);
|
|
8559
|
-
if (!
|
|
8999
|
+
if (!existsSync21(sidecarPath))
|
|
8560
9000
|
return null;
|
|
8561
9001
|
const headerDigest = mapText.match(/^# sources-digest:\s*([a-f0-9]+)/m)?.[1];
|
|
8562
9002
|
if (!headerDigest)
|
|
8563
9003
|
return null;
|
|
8564
9004
|
try {
|
|
8565
|
-
const raw = JSON.parse(
|
|
9005
|
+
const raw = JSON.parse(readFileSync20(sidecarPath, "utf8"));
|
|
8566
9006
|
if (raw && raw.sourcesDigest === headerDigest && Array.isArray(raw.entries)) {
|
|
8567
9007
|
return raw.entries;
|
|
8568
9008
|
}
|
|
@@ -8571,10 +9011,10 @@ function tryLoadSidecar(mapPath, mapText) {
|
|
|
8571
9011
|
return null;
|
|
8572
9012
|
}
|
|
8573
9013
|
function loadCodeMapEntries(mapPath) {
|
|
8574
|
-
if (!
|
|
9014
|
+
if (!existsSync21(mapPath)) {
|
|
8575
9015
|
throw new Error(`${mapPath} is missing; run \`cdd-kit code-map\` first.`);
|
|
8576
9016
|
}
|
|
8577
|
-
const text =
|
|
9017
|
+
const text = readFileSync20(mapPath, "utf8");
|
|
8578
9018
|
const fromSidecar = tryLoadSidecar(mapPath, text);
|
|
8579
9019
|
if (fromSidecar)
|
|
8580
9020
|
return fromSidecar;
|
|
@@ -8946,7 +9386,7 @@ var init_builder = __esm({
|
|
|
8946
9386
|
});
|
|
8947
9387
|
|
|
8948
9388
|
// src/code-graph/reader.ts
|
|
8949
|
-
import { existsSync as
|
|
9389
|
+
import { existsSync as existsSync22, readFileSync as readFileSync21 } from "fs";
|
|
8950
9390
|
function graphPathFor(mapPath) {
|
|
8951
9391
|
const match = mapPath.match(/^(.*?)(?:code-map)(.*)\.ya?ml$/i);
|
|
8952
9392
|
if (match)
|
|
@@ -8954,10 +9394,10 @@ function graphPathFor(mapPath) {
|
|
|
8954
9394
|
return `${mapPath.replace(/\.ya?ml$/i, "")}.graph.json`;
|
|
8955
9395
|
}
|
|
8956
9396
|
function loadCodeGraph(graphPath) {
|
|
8957
|
-
if (!
|
|
9397
|
+
if (!existsSync22(graphPath)) {
|
|
8958
9398
|
throw new Error(`${graphPath} is missing; run \`cdd-kit code-map\` first.`);
|
|
8959
9399
|
}
|
|
8960
|
-
const raw = JSON.parse(
|
|
9400
|
+
const raw = JSON.parse(readFileSync21(graphPath, "utf8"));
|
|
8961
9401
|
if (!raw || raw.schema_version !== "1.0" || !Array.isArray(raw.nodes) || !Array.isArray(raw.edges)) {
|
|
8962
9402
|
throw new Error(`${graphPath} is not a cdd-kit code graph v1 index.`);
|
|
8963
9403
|
}
|
|
@@ -8975,9 +9415,9 @@ __export(worker_dispatch_exports, {
|
|
|
8975
9415
|
scanLangWithWorkers: () => scanLangWithWorkers
|
|
8976
9416
|
});
|
|
8977
9417
|
import { execFile } from "child_process";
|
|
8978
|
-
import { writeFileSync as
|
|
9418
|
+
import { writeFileSync as writeFileSync12, unlinkSync } from "fs";
|
|
8979
9419
|
import { randomBytes } from "crypto";
|
|
8980
|
-
import { join as
|
|
9420
|
+
import { join as join22 } from "path";
|
|
8981
9421
|
import { tmpdir } from "os";
|
|
8982
9422
|
function chunk(arr, parts) {
|
|
8983
9423
|
const size = Math.ceil(arr.length / parts);
|
|
@@ -8987,15 +9427,15 @@ function chunk(arr, parts) {
|
|
|
8987
9427
|
return out;
|
|
8988
9428
|
}
|
|
8989
9429
|
function scanChunkInChild(cliEntry, lang, files, repoRoot) {
|
|
8990
|
-
return new Promise((
|
|
9430
|
+
return new Promise((resolve6, reject) => {
|
|
8991
9431
|
if (!ALLOWED_LANGS.has(lang)) {
|
|
8992
9432
|
return reject(new Error(`refusing to spawn worker for unknown lang: ${lang}`));
|
|
8993
9433
|
}
|
|
8994
|
-
const listFile =
|
|
9434
|
+
const listFile = join22(
|
|
8995
9435
|
tmpdir(),
|
|
8996
9436
|
`cdd-cm-worker-${process.pid}-${randomBytes(12).toString("hex")}.txt`
|
|
8997
9437
|
);
|
|
8998
|
-
|
|
9438
|
+
writeFileSync12(listFile, files.join("\n") + "\n", { encoding: "utf8", mode: 384 });
|
|
8999
9439
|
execFile(
|
|
9000
9440
|
process.execPath,
|
|
9001
9441
|
[cliEntry, "__code-map-scan", "--lang", lang, "--batch-file", listFile, "--repo-root", repoRoot],
|
|
@@ -9009,7 +9449,7 @@ function scanChunkInChild(cliEntry, lang, files, repoRoot) {
|
|
|
9009
9449
|
return reject(err);
|
|
9010
9450
|
try {
|
|
9011
9451
|
const parsed = JSON.parse(stdout);
|
|
9012
|
-
|
|
9452
|
+
resolve6({ entries: parsed.entries ?? [], warnings: parsed.warnings ?? [] });
|
|
9013
9453
|
} catch (e) {
|
|
9014
9454
|
reject(e);
|
|
9015
9455
|
}
|
|
@@ -9067,15 +9507,15 @@ var python_exports = {};
|
|
|
9067
9507
|
__export(python_exports, {
|
|
9068
9508
|
pythonScanner: () => pythonScanner
|
|
9069
9509
|
});
|
|
9070
|
-
import { spawnSync as
|
|
9071
|
-
import { writeFileSync as
|
|
9510
|
+
import { spawnSync as spawnSync4 } from "child_process";
|
|
9511
|
+
import { writeFileSync as writeFileSync13, unlinkSync as unlinkSync2 } from "fs";
|
|
9072
9512
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
9073
|
-
import { join as
|
|
9513
|
+
import { join as join23 } from "path";
|
|
9074
9514
|
import { tmpdir as tmpdir2 } from "os";
|
|
9075
9515
|
function detectPython2() {
|
|
9076
9516
|
for (const candidate of ["python3", "python"]) {
|
|
9077
9517
|
try {
|
|
9078
|
-
const result =
|
|
9518
|
+
const result = spawnSync4(candidate, ["--version"], {
|
|
9079
9519
|
encoding: "utf8",
|
|
9080
9520
|
timeout: 5e3
|
|
9081
9521
|
});
|
|
@@ -9144,13 +9584,13 @@ var init_python = __esm({
|
|
|
9144
9584
|
const entries = [];
|
|
9145
9585
|
const warnings = [];
|
|
9146
9586
|
const scriptPath = ASSET.codeMapPython;
|
|
9147
|
-
const listFile =
|
|
9148
|
-
|
|
9587
|
+
const listFile = join23(tmpdir2(), `cdd-codemap-${process.pid}-${randomBytes2(12).toString("hex")}.txt`);
|
|
9588
|
+
writeFileSync13(listFile, absolutePaths.join("\n") + "\n", { encoding: "utf8", mode: 384 });
|
|
9149
9589
|
let stdout = "";
|
|
9150
9590
|
let stderr = "";
|
|
9151
9591
|
let exitCode = 0;
|
|
9152
9592
|
try {
|
|
9153
|
-
const result =
|
|
9593
|
+
const result = spawnSync4(
|
|
9154
9594
|
interpreter,
|
|
9155
9595
|
[scriptPath, "--batch-file", listFile, "--repo-root", repoRoot],
|
|
9156
9596
|
{
|
|
@@ -9214,7 +9654,7 @@ var init_python = __esm({
|
|
|
9214
9654
|
}
|
|
9215
9655
|
const r = parsed;
|
|
9216
9656
|
entries.push({
|
|
9217
|
-
path: canonicalRelPath(
|
|
9657
|
+
path: canonicalRelPath(join23(repoRoot, r.path), repoRoot),
|
|
9218
9658
|
total_lines: r.total_lines,
|
|
9219
9659
|
imports: r.imports ?? [],
|
|
9220
9660
|
constants: r.constants ?? [],
|
|
@@ -9264,7 +9704,7 @@ __export(javascript_exports, {
|
|
|
9264
9704
|
parseJsSource: () => parseJsSource,
|
|
9265
9705
|
parseSourceWithPlugins: () => parseSourceWithPlugins
|
|
9266
9706
|
});
|
|
9267
|
-
import { readFileSync as
|
|
9707
|
+
import { readFileSync as readFileSync23 } from "fs";
|
|
9268
9708
|
import { parse } from "@babel/parser";
|
|
9269
9709
|
function parseSourceWithPlugins(source, plugins) {
|
|
9270
9710
|
return parse(source, {
|
|
@@ -9667,7 +10107,7 @@ var init_javascript = __esm({
|
|
|
9667
10107
|
async scan(absolutePath, repoRoot) {
|
|
9668
10108
|
let source;
|
|
9669
10109
|
try {
|
|
9670
|
-
source =
|
|
10110
|
+
source = readFileSync23(absolutePath, "utf8");
|
|
9671
10111
|
} catch (err) {
|
|
9672
10112
|
throw err;
|
|
9673
10113
|
}
|
|
@@ -9687,7 +10127,7 @@ var typescript_exports = {};
|
|
|
9687
10127
|
__export(typescript_exports, {
|
|
9688
10128
|
tsScanner: () => tsScanner
|
|
9689
10129
|
});
|
|
9690
|
-
import { readFileSync as
|
|
10130
|
+
import { readFileSync as readFileSync24 } from "fs";
|
|
9691
10131
|
var TypeScriptScanner, tsScanner;
|
|
9692
10132
|
var init_typescript = __esm({
|
|
9693
10133
|
"src/code-map/scanners/typescript.ts"() {
|
|
@@ -9699,7 +10139,7 @@ var init_typescript = __esm({
|
|
|
9699
10139
|
async scan(absolutePath, repoRoot) {
|
|
9700
10140
|
let source;
|
|
9701
10141
|
try {
|
|
9702
|
-
source =
|
|
10142
|
+
source = readFileSync24(absolutePath, "utf8");
|
|
9703
10143
|
} catch (err) {
|
|
9704
10144
|
throw err;
|
|
9705
10145
|
}
|
|
@@ -9737,7 +10177,7 @@ var vue_exports = {};
|
|
|
9737
10177
|
__export(vue_exports, {
|
|
9738
10178
|
vueScanner: () => vueScanner
|
|
9739
10179
|
});
|
|
9740
|
-
import { readFileSync as
|
|
10180
|
+
import { readFileSync as readFileSync25 } from "fs";
|
|
9741
10181
|
import { parse as parse2 } from "@vue/compiler-sfc";
|
|
9742
10182
|
var VueScanner, vueScanner;
|
|
9743
10183
|
var init_vue = __esm({
|
|
@@ -9750,7 +10190,7 @@ var init_vue = __esm({
|
|
|
9750
10190
|
async scan(absolutePath, repoRoot) {
|
|
9751
10191
|
let source;
|
|
9752
10192
|
try {
|
|
9753
|
-
source =
|
|
10193
|
+
source = readFileSync25(absolutePath, "utf8");
|
|
9754
10194
|
} catch (err) {
|
|
9755
10195
|
throw err;
|
|
9756
10196
|
}
|
|
@@ -9845,14 +10285,15 @@ var init_vue = __esm({
|
|
|
9845
10285
|
var code_map_exports = {};
|
|
9846
10286
|
__export(code_map_exports, {
|
|
9847
10287
|
codeMap: () => codeMap,
|
|
9848
|
-
computeSourcesDigest: () => computeSourcesDigest
|
|
10288
|
+
computeSourcesDigest: () => computeSourcesDigest,
|
|
10289
|
+
slugifySurface: () => slugifySurface
|
|
9849
10290
|
});
|
|
9850
|
-
import { existsSync as
|
|
9851
|
-
import { resolve, dirname as
|
|
10291
|
+
import { existsSync as existsSync23, mkdirSync as mkdirSync10, readFileSync as readFileSync26, writeFileSync as writeFileSync14 } from "fs";
|
|
10292
|
+
import { resolve as resolve3, dirname as dirname6, relative as relative6 } from "path";
|
|
9852
10293
|
import { createHash as createHash6 } from "crypto";
|
|
9853
10294
|
import { createRequire } from "module";
|
|
9854
10295
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
9855
|
-
import { join as
|
|
10296
|
+
import { join as join24 } from "path";
|
|
9856
10297
|
function computeSourcesDigest(absolutePaths, cwd) {
|
|
9857
10298
|
const lines = absolutePaths.slice().sort().map((p) => {
|
|
9858
10299
|
const rel = relative6(cwd, p).replace(/\\/g, "/");
|
|
@@ -9867,14 +10308,15 @@ function slugifySurface(surface) {
|
|
|
9867
10308
|
async function codeMap(opts) {
|
|
9868
10309
|
const start = Date.now();
|
|
9869
10310
|
if (opts.surface) {
|
|
9870
|
-
const resolvedSurface =
|
|
9871
|
-
if (!
|
|
10311
|
+
const resolvedSurface = resolve3(process.cwd(), opts.surface);
|
|
10312
|
+
if (!existsSync23(resolvedSurface)) {
|
|
9872
10313
|
log.error(`code-map --surface path not found: ${opts.surface}`);
|
|
9873
10314
|
return 1;
|
|
9874
10315
|
}
|
|
9875
10316
|
}
|
|
9876
10317
|
const scanPath = opts.surface ?? opts.path;
|
|
9877
|
-
const root =
|
|
10318
|
+
const root = resolve3(process.cwd(), scanPath);
|
|
10319
|
+
const surfaceRoot = opts.surface ? relative6(process.cwd(), root).replace(/\\/g, "/") || "." : void 0;
|
|
9878
10320
|
const out = opts.out ?? (opts.surface ? `.cdd/code-map.${slugifySurface(opts.surface)}.yml` : ".cdd/code-map.yml");
|
|
9879
10321
|
let cfg;
|
|
9880
10322
|
try {
|
|
@@ -9942,7 +10384,8 @@ async function codeMap(opts) {
|
|
|
9942
10384
|
const sourcesDigest = computeSourcesDigest(files, root);
|
|
9943
10385
|
const yamlBody = renderYaml(result.entries, {
|
|
9944
10386
|
generator: `cdd-kit ${_pkg.version}`,
|
|
9945
|
-
sourcesDigest
|
|
10387
|
+
sourcesDigest,
|
|
10388
|
+
surfaceRoot
|
|
9946
10389
|
});
|
|
9947
10390
|
const totalSrc = result.entries.reduce((s, e) => s + e.total_lines, 0);
|
|
9948
10391
|
const mapLines = yamlBody.split("\n").length;
|
|
@@ -9953,7 +10396,7 @@ async function codeMap(opts) {
|
|
|
9953
10396
|
log.warn(`${w.path}: ${w.message}`);
|
|
9954
10397
|
}
|
|
9955
10398
|
if (opts.check) {
|
|
9956
|
-
const existing =
|
|
10399
|
+
const existing = existsSync23(out) ? readFileSync26(out, "utf8") : "";
|
|
9957
10400
|
const normalize = (s) => s.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/^# generated: [^\n]+\n/m, "# generated: <normalized>\n");
|
|
9958
10401
|
if (normalize(existing) !== normalize(yamlBody)) {
|
|
9959
10402
|
if (!opts.silent)
|
|
@@ -9964,11 +10407,11 @@ async function codeMap(opts) {
|
|
|
9964
10407
|
log.ok(`code-map up to date: ${out}`);
|
|
9965
10408
|
return 0;
|
|
9966
10409
|
}
|
|
9967
|
-
|
|
9968
|
-
|
|
10410
|
+
mkdirSync10(dirname6(out), { recursive: true });
|
|
10411
|
+
writeFileSync14(out, yamlBody, "utf8");
|
|
9969
10412
|
try {
|
|
9970
10413
|
const sidecarPath = sidecarPathFor(out);
|
|
9971
|
-
|
|
10414
|
+
writeFileSync14(sidecarPath, JSON.stringify({ sourcesDigest, entries: result.entries }), "utf8");
|
|
9972
10415
|
const rel = relative6(process.cwd(), sidecarPath).replace(/\\/g, "/");
|
|
9973
10416
|
if (!rel.startsWith("..")) {
|
|
9974
10417
|
ensureGitignoreEntry(process.cwd(), rel, "cdd-kit local cache (do not commit)");
|
|
@@ -9978,7 +10421,7 @@ async function codeMap(opts) {
|
|
|
9978
10421
|
generator: `cdd-kit ${_pkg.version}`,
|
|
9979
10422
|
sourcesDigest
|
|
9980
10423
|
});
|
|
9981
|
-
|
|
10424
|
+
writeFileSync14(graphPath, JSON.stringify(graph2, null, 2) + "\n", "utf8");
|
|
9982
10425
|
const graphRel = relative6(process.cwd(), graphPath).replace(/\\/g, "/");
|
|
9983
10426
|
if (!graphRel.startsWith("..")) {
|
|
9984
10427
|
ensureGitignoreEntry(process.cwd(), graphRel, "cdd-kit local cache (do not commit)");
|
|
@@ -10003,8 +10446,8 @@ var init_code_map = __esm({
|
|
|
10003
10446
|
init_digest();
|
|
10004
10447
|
init_gitignore();
|
|
10005
10448
|
_require = createRequire(import.meta.url);
|
|
10006
|
-
_pkgPath =
|
|
10007
|
-
_pkg = JSON.parse(
|
|
10449
|
+
_pkgPath = join24(fileURLToPath2(import.meta.url), "..", "..", "..", "package.json");
|
|
10450
|
+
_pkg = JSON.parse(readFileSync26(_pkgPath, "utf8"));
|
|
10008
10451
|
}
|
|
10009
10452
|
});
|
|
10010
10453
|
|
|
@@ -10013,15 +10456,15 @@ var refresh_exports = {};
|
|
|
10013
10456
|
__export(refresh_exports, {
|
|
10014
10457
|
refresh: () => refresh
|
|
10015
10458
|
});
|
|
10016
|
-
import { existsSync as
|
|
10017
|
-
import { dirname as
|
|
10459
|
+
import { existsSync as existsSync24, mkdirSync as mkdirSync11, readdirSync as readdirSync10, copyFileSync as copyFileSync5, readFileSync as readFileSync27, writeFileSync as writeFileSync15 } from "fs";
|
|
10460
|
+
import { dirname as dirname7, join as join25, relative as relative7 } from "path";
|
|
10018
10461
|
import { createHash as createHash7 } from "crypto";
|
|
10019
10462
|
function fileHash2(filePath) {
|
|
10020
|
-
return createHash7("sha256").update(
|
|
10463
|
+
return createHash7("sha256").update(readFileSync27(filePath)).digest("hex");
|
|
10021
10464
|
}
|
|
10022
10465
|
function planForceRefresh(srcDir, destDir, sectionLabel) {
|
|
10023
10466
|
const plan = [];
|
|
10024
|
-
if (!
|
|
10467
|
+
if (!existsSync24(srcDir))
|
|
10025
10468
|
return plan;
|
|
10026
10469
|
function walk(curSrc, curDest) {
|
|
10027
10470
|
let items;
|
|
@@ -10031,16 +10474,16 @@ function planForceRefresh(srcDir, destDir, sectionLabel) {
|
|
|
10031
10474
|
return;
|
|
10032
10475
|
}
|
|
10033
10476
|
for (const item of items) {
|
|
10034
|
-
const sp =
|
|
10035
|
-
const dp =
|
|
10477
|
+
const sp = join25(curSrc, item.name);
|
|
10478
|
+
const dp = join25(curDest, item.name);
|
|
10036
10479
|
if (item.isDirectory()) {
|
|
10037
10480
|
walk(sp, dp);
|
|
10038
10481
|
continue;
|
|
10039
10482
|
}
|
|
10040
10483
|
if (!item.isFile())
|
|
10041
10484
|
continue;
|
|
10042
|
-
const rel =
|
|
10043
|
-
if (!
|
|
10485
|
+
const rel = join25(sectionLabel, relative7(srcDir, sp)).replace(/\\/g, "/");
|
|
10486
|
+
if (!existsSync24(dp)) {
|
|
10044
10487
|
plan.push({ src: sp, dest: dp, rel, action: "add" });
|
|
10045
10488
|
} else if (fileHash2(sp) !== fileHash2(dp)) {
|
|
10046
10489
|
plan.push({ src: sp, dest: dp, rel, action: "overwrite" });
|
|
@@ -10053,9 +10496,9 @@ function planForceRefresh(srcDir, destDir, sectionLabel) {
|
|
|
10053
10496
|
return plan;
|
|
10054
10497
|
}
|
|
10055
10498
|
function planSingleFile(src, dest, rel) {
|
|
10056
|
-
if (!
|
|
10499
|
+
if (!existsSync24(src))
|
|
10057
10500
|
return null;
|
|
10058
|
-
if (!
|
|
10501
|
+
if (!existsSync24(dest))
|
|
10059
10502
|
return { src, dest, rel, action: "add" };
|
|
10060
10503
|
if (fileHash2(src) !== fileHash2(dest))
|
|
10061
10504
|
return { src, dest, rel, action: "overwrite" };
|
|
@@ -10068,15 +10511,15 @@ function applyPlan(plan, backupRoot) {
|
|
|
10068
10511
|
if (item.action === "skip")
|
|
10069
10512
|
continue;
|
|
10070
10513
|
if (item.action === "overwrite") {
|
|
10071
|
-
const backupPath =
|
|
10072
|
-
|
|
10073
|
-
|
|
10514
|
+
const backupPath = join25(backupRoot, item.rel);
|
|
10515
|
+
mkdirSync11(dirname7(backupPath), { recursive: true });
|
|
10516
|
+
copyFileSync5(item.dest, backupPath);
|
|
10074
10517
|
overwritten += 1;
|
|
10075
10518
|
} else {
|
|
10076
10519
|
added += 1;
|
|
10077
10520
|
}
|
|
10078
|
-
|
|
10079
|
-
|
|
10521
|
+
mkdirSync11(dirname7(item.dest), { recursive: true });
|
|
10522
|
+
copyFileSync5(item.src, item.dest);
|
|
10080
10523
|
}
|
|
10081
10524
|
return { added, overwritten };
|
|
10082
10525
|
}
|
|
@@ -10097,14 +10540,14 @@ function parseAgentFrontmatter(content) {
|
|
|
10097
10540
|
return fm;
|
|
10098
10541
|
}
|
|
10099
10542
|
function resyncModelPolicy(cwd) {
|
|
10100
|
-
const policyPath =
|
|
10543
|
+
const policyPath = join25(cwd, ".cdd", "model-policy.json");
|
|
10101
10544
|
const result = { changed: false, diff: [], policyPath };
|
|
10102
|
-
if (!
|
|
10545
|
+
if (!existsSync24(AGENTS_HOME))
|
|
10103
10546
|
return result;
|
|
10104
10547
|
const desired = {};
|
|
10105
10548
|
const agentFiles = readdirSync10(AGENTS_HOME, { withFileTypes: true }).filter((d) => d.isFile() && d.name.endsWith(".md"));
|
|
10106
10549
|
for (const f of agentFiles) {
|
|
10107
|
-
const content =
|
|
10550
|
+
const content = readFileSync27(join25(AGENTS_HOME, f.name), "utf8");
|
|
10108
10551
|
const fm = parseAgentFrontmatter(content);
|
|
10109
10552
|
if (fm.name && fm.model)
|
|
10110
10553
|
desired[fm.name] = fm.model;
|
|
@@ -10112,9 +10555,9 @@ function resyncModelPolicy(cwd) {
|
|
|
10112
10555
|
if (Object.keys(desired).length === 0)
|
|
10113
10556
|
return result;
|
|
10114
10557
|
let existing = {};
|
|
10115
|
-
if (
|
|
10558
|
+
if (existsSync24(policyPath)) {
|
|
10116
10559
|
try {
|
|
10117
|
-
existing = JSON.parse(
|
|
10560
|
+
existing = JSON.parse(readFileSync27(policyPath, "utf8"));
|
|
10118
10561
|
} catch {
|
|
10119
10562
|
}
|
|
10120
10563
|
}
|
|
@@ -10134,8 +10577,8 @@ function resyncModelPolicy(cwd) {
|
|
|
10134
10577
|
merged["schema-version"] = "0.2.0";
|
|
10135
10578
|
if (!("provider" in merged))
|
|
10136
10579
|
merged["provider"] = "claude";
|
|
10137
|
-
|
|
10138
|
-
|
|
10580
|
+
mkdirSync11(dirname7(policyPath), { recursive: true });
|
|
10581
|
+
writeFileSync15(policyPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
10139
10582
|
result.changed = true;
|
|
10140
10583
|
return result;
|
|
10141
10584
|
}
|
|
@@ -10143,21 +10586,21 @@ function planTemplateRefresh(cwd) {
|
|
|
10143
10586
|
const sections = [];
|
|
10144
10587
|
sections.push({
|
|
10145
10588
|
name: "specs/templates",
|
|
10146
|
-
plan: planForceRefresh(ASSET.specsTemplates,
|
|
10589
|
+
plan: planForceRefresh(ASSET.specsTemplates, join25(cwd, "specs", "templates"), "specs/templates")
|
|
10147
10590
|
});
|
|
10148
10591
|
sections.push({
|
|
10149
10592
|
name: "tests/templates",
|
|
10150
|
-
plan: planForceRefresh(ASSET.testsTemplates,
|
|
10593
|
+
plan: planForceRefresh(ASSET.testsTemplates, join25(cwd, "tests", "templates"), "tests/templates")
|
|
10151
10594
|
});
|
|
10152
|
-
const ciTemplatesAsset =
|
|
10153
|
-
if (
|
|
10595
|
+
const ciTemplatesAsset = join25(ASSET.ci, "..", "ci-templates");
|
|
10596
|
+
if (existsSync24(ciTemplatesAsset)) {
|
|
10154
10597
|
sections.push({
|
|
10155
10598
|
name: "ci-templates",
|
|
10156
|
-
plan: planForceRefresh(ciTemplatesAsset,
|
|
10599
|
+
plan: planForceRefresh(ciTemplatesAsset, join25(cwd, "ci-templates"), "ci-templates")
|
|
10157
10600
|
});
|
|
10158
10601
|
}
|
|
10159
|
-
const wfAsset =
|
|
10160
|
-
const wfDest =
|
|
10602
|
+
const wfAsset = join25(ASSET.githubWorkflows, "contract-driven-gates.yml");
|
|
10603
|
+
const wfDest = join25(cwd, ".github", "workflows", "contract-driven-gates.yml");
|
|
10161
10604
|
const wfPlan = planSingleFile(wfAsset, wfDest, ".github/workflows/contract-driven-gates.yml");
|
|
10162
10605
|
if (wfPlan)
|
|
10163
10606
|
sections.push({ name: ".github/workflows/contract-driven-gates.yml", plan: [wfPlan] });
|
|
@@ -10212,7 +10655,7 @@ async function refresh(opts) {
|
|
|
10212
10655
|
}
|
|
10213
10656
|
if (apply) {
|
|
10214
10657
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
10215
|
-
backupRoot =
|
|
10658
|
+
backupRoot = join25(cwd, ".cdd", ".refresh-backup", ts);
|
|
10216
10659
|
const result = applyPlan(total, backupRoot);
|
|
10217
10660
|
templateAdded = result.added;
|
|
10218
10661
|
templateOverwritten = result.overwritten;
|
|
@@ -10232,8 +10675,8 @@ async function refresh(opts) {
|
|
|
10232
10675
|
}
|
|
10233
10676
|
log.blank();
|
|
10234
10677
|
if (!opts.noHooks) {
|
|
10235
|
-
const markerPath =
|
|
10236
|
-
if (
|
|
10678
|
+
const markerPath = join25(cwd, HOOKS_MARKER_PATH);
|
|
10679
|
+
if (existsSync24(markerPath)) {
|
|
10237
10680
|
log.info("[4/6] re-install code-map pre-commit hook (marker found)");
|
|
10238
10681
|
if (apply) {
|
|
10239
10682
|
try {
|
|
@@ -10329,8 +10772,8 @@ __export(lint_agents_exports, {
|
|
|
10329
10772
|
lintAgentContent: () => lintAgentContent,
|
|
10330
10773
|
lintAgents: () => lintAgents
|
|
10331
10774
|
});
|
|
10332
|
-
import { readdirSync as readdirSync11, readFileSync as
|
|
10333
|
-
import { join as
|
|
10775
|
+
import { readdirSync as readdirSync11, readFileSync as readFileSync28 } from "fs";
|
|
10776
|
+
import { join as join26 } from "path";
|
|
10334
10777
|
import { load as yamlLoad2 } from "js-yaml";
|
|
10335
10778
|
function extractRequiredArtifactsSection(content) {
|
|
10336
10779
|
const match = content.match(
|
|
@@ -10459,7 +10902,7 @@ function lintAgentContent(filename, rawContent, opts = {}) {
|
|
|
10459
10902
|
return violations;
|
|
10460
10903
|
}
|
|
10461
10904
|
function collectAgentViolations(cwd, opts = {}) {
|
|
10462
|
-
const agentsDir =
|
|
10905
|
+
const agentsDir = join26(cwd, ".claude", "agents");
|
|
10463
10906
|
let files;
|
|
10464
10907
|
try {
|
|
10465
10908
|
files = readdirSync11(agentsDir).filter((f) => f.endsWith(".md")).sort();
|
|
@@ -10470,7 +10913,7 @@ function collectAgentViolations(cwd, opts = {}) {
|
|
|
10470
10913
|
for (const filename of files) {
|
|
10471
10914
|
let content;
|
|
10472
10915
|
try {
|
|
10473
|
-
content =
|
|
10916
|
+
content = readFileSync28(join26(agentsDir, filename), "utf8");
|
|
10474
10917
|
} catch {
|
|
10475
10918
|
violations.push({
|
|
10476
10919
|
file: filename,
|
|
@@ -10489,7 +10932,7 @@ async function lintAgents(opts) {
|
|
|
10489
10932
|
const violations = collectAgentViolations(cwd, opts);
|
|
10490
10933
|
if (violations === null) {
|
|
10491
10934
|
log.error(
|
|
10492
|
-
`lint-agents: cannot read ${
|
|
10935
|
+
`lint-agents: cannot read ${join26(cwd, ".claude", "agents")} -- is this a cdd-kit project?`
|
|
10493
10936
|
);
|
|
10494
10937
|
return 1;
|
|
10495
10938
|
}
|
|
@@ -10514,22 +10957,132 @@ var init_lint_agents = __esm({
|
|
|
10514
10957
|
}
|
|
10515
10958
|
});
|
|
10516
10959
|
|
|
10960
|
+
// src/commands/chokepoints.ts
|
|
10961
|
+
import { existsSync as existsSync25, readFileSync as readFileSync29, readdirSync as readdirSync12 } from "fs";
|
|
10962
|
+
import { join as join27, resolve as resolve4 } from "path";
|
|
10963
|
+
import { spawnSync as spawnSync5 } from "child_process";
|
|
10964
|
+
function safeRead2(path) {
|
|
10965
|
+
try {
|
|
10966
|
+
return readFileSync29(path, "utf8");
|
|
10967
|
+
} catch {
|
|
10968
|
+
return "";
|
|
10969
|
+
}
|
|
10970
|
+
}
|
|
10971
|
+
function probeGraphFirst(cwd) {
|
|
10972
|
+
const settingsPath = join27(cwd, ".claude", "settings.json");
|
|
10973
|
+
let live = false;
|
|
10974
|
+
if (existsSync25(settingsPath)) {
|
|
10975
|
+
try {
|
|
10976
|
+
const settings = JSON.parse(safeRead2(settingsPath));
|
|
10977
|
+
const entries = settings.hooks?.PreToolUse;
|
|
10978
|
+
if (Array.isArray(entries)) {
|
|
10979
|
+
live = entries.some(
|
|
10980
|
+
(e) => Array.isArray(e?.hooks) && e.hooks.some((h) => typeof h?.command === "string" && h.command.includes(GRAPH_FIRST_MARKER))
|
|
10981
|
+
);
|
|
10982
|
+
}
|
|
10983
|
+
} catch {
|
|
10984
|
+
}
|
|
10985
|
+
}
|
|
10986
|
+
return {
|
|
10987
|
+
id: "graph-first-hook",
|
|
10988
|
+
name: "graph-first exploration hook",
|
|
10989
|
+
live,
|
|
10990
|
+
detail: live ? "PreToolUse hook steers agents to graph/index queries before Read" : "dormant \u2014 run `cdd-kit install-agent-hooks --graph-first advisory` to stop agents defaulting to Read"
|
|
10991
|
+
};
|
|
10992
|
+
}
|
|
10993
|
+
function resolveHooksDir2(cwd) {
|
|
10994
|
+
try {
|
|
10995
|
+
const res = spawnSync5("git", ["rev-parse", "--git-path", "hooks"], { cwd, encoding: "utf8" });
|
|
10996
|
+
if (res.status === 0 && res.stdout.trim())
|
|
10997
|
+
return resolve4(cwd, res.stdout.trim());
|
|
10998
|
+
} catch {
|
|
10999
|
+
}
|
|
11000
|
+
return join27(cwd, ".git", "hooks");
|
|
11001
|
+
}
|
|
11002
|
+
function probePreCommitGate(cwd) {
|
|
11003
|
+
const hookPath = join27(resolveHooksDir2(cwd), "pre-commit");
|
|
11004
|
+
const live = existsSync25(hookPath) && safeRead2(hookPath).includes(PRECOMMIT_MARKER);
|
|
11005
|
+
return {
|
|
11006
|
+
id: "pre-commit-gate",
|
|
11007
|
+
name: "pre-commit gate hook",
|
|
11008
|
+
live,
|
|
11009
|
+
detail: live ? "`cdd-kit gate` runs before each commit touching specs/contracts" : "dormant \u2014 run `cdd-kit install-hooks` to block commits that fail the gate"
|
|
11010
|
+
};
|
|
11011
|
+
}
|
|
11012
|
+
function probeOpenApiGate(cwd) {
|
|
11013
|
+
let where = "";
|
|
11014
|
+
const pkgPath = join27(cwd, "package.json");
|
|
11015
|
+
if (existsSync25(pkgPath)) {
|
|
11016
|
+
try {
|
|
11017
|
+
const pkg2 = JSON.parse(safeRead2(pkgPath));
|
|
11018
|
+
const scripts = pkg2.scripts ?? {};
|
|
11019
|
+
for (const [name, cmd] of Object.entries(scripts)) {
|
|
11020
|
+
if (typeof cmd === "string" && cmd.includes(OPENAPI_CHECK_MARKER)) {
|
|
11021
|
+
where = `package.json script \`${name}\``;
|
|
11022
|
+
break;
|
|
11023
|
+
}
|
|
11024
|
+
}
|
|
11025
|
+
} catch {
|
|
11026
|
+
}
|
|
11027
|
+
}
|
|
11028
|
+
if (!where) {
|
|
11029
|
+
const wfDir = join27(cwd, ".github", "workflows");
|
|
11030
|
+
if (existsSync25(wfDir)) {
|
|
11031
|
+
try {
|
|
11032
|
+
for (const entry of readdirSync12(wfDir)) {
|
|
11033
|
+
if (!/\.ya?ml$/.test(entry))
|
|
11034
|
+
continue;
|
|
11035
|
+
if (safeRead2(join27(wfDir, entry)).includes(OPENAPI_CHECK_MARKER)) {
|
|
11036
|
+
where = `CI workflow ${entry}`;
|
|
11037
|
+
break;
|
|
11038
|
+
}
|
|
11039
|
+
}
|
|
11040
|
+
} catch {
|
|
11041
|
+
}
|
|
11042
|
+
}
|
|
11043
|
+
}
|
|
11044
|
+
const live = where !== "";
|
|
11045
|
+
return {
|
|
11046
|
+
id: "openapi-sync-gate",
|
|
11047
|
+
name: "OpenAPI sync gate",
|
|
11048
|
+
live,
|
|
11049
|
+
detail: live ? `\`cdd-kit openapi export --check\` runs in ${where}` : "dormant \u2014 wire `cdd-kit openapi export --check --out <artifact>` into CI or a package.json script (or run `cdd-kit init`)"
|
|
11050
|
+
};
|
|
11051
|
+
}
|
|
11052
|
+
function detectChokepoints(cwd) {
|
|
11053
|
+
return [
|
|
11054
|
+
probeGraphFirst(cwd),
|
|
11055
|
+
probePreCommitGate(cwd),
|
|
11056
|
+
probeOpenApiGate(cwd)
|
|
11057
|
+
];
|
|
11058
|
+
}
|
|
11059
|
+
var GRAPH_FIRST_MARKER, PRECOMMIT_MARKER, OPENAPI_CHECK_MARKER;
|
|
11060
|
+
var init_chokepoints = __esm({
|
|
11061
|
+
"src/commands/chokepoints.ts"() {
|
|
11062
|
+
"use strict";
|
|
11063
|
+
GRAPH_FIRST_MARKER = "pre-tool-use-graph-first";
|
|
11064
|
+
PRECOMMIT_MARKER = "# cdd-kit-managed-block-start";
|
|
11065
|
+
OPENAPI_CHECK_MARKER = "openapi export --check";
|
|
11066
|
+
}
|
|
11067
|
+
});
|
|
11068
|
+
|
|
10517
11069
|
// src/commands/doctor.ts
|
|
10518
11070
|
var doctor_exports = {};
|
|
10519
11071
|
__export(doctor_exports, {
|
|
10520
11072
|
doctor: () => doctor
|
|
10521
11073
|
});
|
|
10522
|
-
import { existsSync as
|
|
11074
|
+
import { existsSync as existsSync26, readdirSync as readdirSync13, readFileSync as readFileSync30 } from "fs";
|
|
10523
11075
|
import { createHash as createHash8 } from "crypto";
|
|
10524
|
-
import {
|
|
11076
|
+
import { spawnSync as spawnSync6 } from "child_process";
|
|
11077
|
+
import { join as join28, relative as relative8 } from "path";
|
|
10525
11078
|
function fileExists(cwd, relPath) {
|
|
10526
|
-
return
|
|
11079
|
+
return existsSync26(join28(cwd, relPath));
|
|
10527
11080
|
}
|
|
10528
11081
|
function findFiles(dir, predicate, found = []) {
|
|
10529
|
-
if (!
|
|
11082
|
+
if (!existsSync26(dir))
|
|
10530
11083
|
return found;
|
|
10531
|
-
for (const entry of
|
|
10532
|
-
const fullPath =
|
|
11084
|
+
for (const entry of readdirSync13(dir, { withFileTypes: true })) {
|
|
11085
|
+
const fullPath = join28(dir, entry.name);
|
|
10533
11086
|
if (entry.isDirectory())
|
|
10534
11087
|
findFiles(fullPath, predicate, found);
|
|
10535
11088
|
else if (entry.isFile() && predicate(entry.name))
|
|
@@ -10545,9 +11098,9 @@ function inputDigest(paths, cwd) {
|
|
|
10545
11098
|
return createHash8("sha256").update(combined).digest("hex");
|
|
10546
11099
|
}
|
|
10547
11100
|
function readContextIndexMetadata(filePath) {
|
|
10548
|
-
if (!
|
|
11101
|
+
if (!existsSync26(filePath))
|
|
10549
11102
|
return {};
|
|
10550
|
-
const text =
|
|
11103
|
+
const text = readFileSync30(filePath, "utf8");
|
|
10551
11104
|
const out = {};
|
|
10552
11105
|
const digestMatch = text.match(/^inputs-digest:\s*([a-f0-9]+)/m);
|
|
10553
11106
|
if (digestMatch)
|
|
@@ -10559,14 +11112,14 @@ function readContextIndexMetadata(filePath) {
|
|
|
10559
11112
|
}
|
|
10560
11113
|
function checkContextFreshness(cwd) {
|
|
10561
11114
|
const findings = [];
|
|
10562
|
-
const projectMap =
|
|
10563
|
-
const contractsIndex =
|
|
10564
|
-
const contextPolicy =
|
|
11115
|
+
const projectMap = join28(cwd, "specs", "context", "project-map.md");
|
|
11116
|
+
const contractsIndex = join28(cwd, "specs", "context", "contracts-index.md");
|
|
11117
|
+
const contextPolicy = join28(cwd, ".cdd", "context-policy.json");
|
|
10565
11118
|
const contractFiles = findFiles(
|
|
10566
|
-
|
|
11119
|
+
join28(cwd, "contracts"),
|
|
10567
11120
|
(name) => name.endsWith(".md") && name !== "INDEX.md" && name !== "CHANGELOG.md"
|
|
10568
11121
|
);
|
|
10569
|
-
if (!
|
|
11122
|
+
if (!existsSync26(projectMap) || !existsSync26(contractsIndex)) {
|
|
10570
11123
|
findings.push({
|
|
10571
11124
|
level: "warning",
|
|
10572
11125
|
message: "specs/context indexes are missing; run cdd-kit context-scan before classification"
|
|
@@ -10575,7 +11128,7 @@ function checkContextFreshness(cwd) {
|
|
|
10575
11128
|
}
|
|
10576
11129
|
const projectMapMeta = readContextIndexMetadata(projectMap);
|
|
10577
11130
|
const contractsIndexMeta = readContextIndexMetadata(contractsIndex);
|
|
10578
|
-
const projectInputDigest = inputDigest([contextPolicy].filter(
|
|
11131
|
+
const projectInputDigest = inputDigest([contextPolicy].filter(existsSync26), cwd);
|
|
10579
11132
|
if (projectMapMeta.inputsDigest === void 0) {
|
|
10580
11133
|
findings.push({
|
|
10581
11134
|
level: "warning",
|
|
@@ -10612,7 +11165,7 @@ function checkContextFreshness(cwd) {
|
|
|
10612
11165
|
}
|
|
10613
11166
|
function readAgentModel(path) {
|
|
10614
11167
|
try {
|
|
10615
|
-
const text =
|
|
11168
|
+
const text = readFileSync30(path, "utf8");
|
|
10616
11169
|
const m = text.match(/^model:\s*(\S+)/m);
|
|
10617
11170
|
return m ? m[1] : null;
|
|
10618
11171
|
} catch {
|
|
@@ -10620,12 +11173,12 @@ function readAgentModel(path) {
|
|
|
10620
11173
|
}
|
|
10621
11174
|
}
|
|
10622
11175
|
function checkModelPolicyDrift(cwd) {
|
|
10623
|
-
const policyPath =
|
|
10624
|
-
if (!
|
|
11176
|
+
const policyPath = join28(cwd, ".cdd", "model-policy.json");
|
|
11177
|
+
if (!existsSync26(policyPath))
|
|
10625
11178
|
return [];
|
|
10626
11179
|
let policy;
|
|
10627
11180
|
try {
|
|
10628
|
-
policy = JSON.parse(
|
|
11181
|
+
policy = JSON.parse(readFileSync30(policyPath, "utf8"));
|
|
10629
11182
|
} catch {
|
|
10630
11183
|
return [{ level: "warning", message: ".cdd/model-policy.json is not valid JSON" }];
|
|
10631
11184
|
}
|
|
@@ -10637,18 +11190,18 @@ function checkModelPolicyDrift(cwd) {
|
|
|
10637
11190
|
}];
|
|
10638
11191
|
}
|
|
10639
11192
|
const candidateDirs = [
|
|
10640
|
-
|
|
10641
|
-
process.env.HOME ?
|
|
10642
|
-
process.env.USERPROFILE ?
|
|
10643
|
-
].filter((p) => p &&
|
|
11193
|
+
join28(cwd, ".claude", "agents"),
|
|
11194
|
+
process.env.HOME ? join28(process.env.HOME, ".claude", "agents") : "",
|
|
11195
|
+
process.env.USERPROFILE ? join28(process.env.USERPROFILE, ".claude", "agents") : ""
|
|
11196
|
+
].filter((p) => p && existsSync26(p));
|
|
10644
11197
|
if (candidateDirs.length === 0)
|
|
10645
11198
|
return [];
|
|
10646
11199
|
const findings = [];
|
|
10647
11200
|
for (const [role, expected] of Object.entries(roles)) {
|
|
10648
11201
|
let foundAny = false;
|
|
10649
11202
|
for (const dir of candidateDirs) {
|
|
10650
|
-
const path =
|
|
10651
|
-
if (!
|
|
11203
|
+
const path = join28(dir, `${role}.md`);
|
|
11204
|
+
if (!existsSync26(path))
|
|
10652
11205
|
continue;
|
|
10653
11206
|
foundAny = true;
|
|
10654
11207
|
const actual = readAgentModel(path);
|
|
@@ -10668,14 +11221,14 @@ function checkModelPolicyDrift(cwd) {
|
|
|
10668
11221
|
return findings;
|
|
10669
11222
|
}
|
|
10670
11223
|
function checkAgentLint(cwd) {
|
|
10671
|
-
const agentsDir =
|
|
10672
|
-
if (!
|
|
11224
|
+
const agentsDir = join28(cwd, ".claude", "agents");
|
|
11225
|
+
if (!existsSync26(agentsDir))
|
|
10673
11226
|
return [];
|
|
10674
11227
|
const violations = collectAgentViolations(cwd);
|
|
10675
11228
|
if (violations === null) {
|
|
10676
11229
|
return [{
|
|
10677
11230
|
level: "warning",
|
|
10678
|
-
message: `lint-agents: could not read ${
|
|
11231
|
+
message: `lint-agents: could not read ${join28(".claude", "agents")} -- agent prompts were not scanned`
|
|
10679
11232
|
}];
|
|
10680
11233
|
}
|
|
10681
11234
|
const findings = violations.map((v) => ({
|
|
@@ -10689,8 +11242,8 @@ function checkAgentLint(cwd) {
|
|
|
10689
11242
|
}
|
|
10690
11243
|
function checkCodeMap(cwd) {
|
|
10691
11244
|
const findings = [];
|
|
10692
|
-
const mapPath =
|
|
10693
|
-
if (!
|
|
11245
|
+
const mapPath = join28(cwd, ".cdd", "code-map.yml");
|
|
11246
|
+
if (!existsSync26(mapPath)) {
|
|
10694
11247
|
const probe2 = checkCodeMapFreshness(cwd);
|
|
10695
11248
|
if (probe2.status === "config-error") {
|
|
10696
11249
|
findings.push({ level: "warning", message: `.cdd/code-map-config.yml is invalid: ${probe2.configError}` });
|
|
@@ -10709,7 +11262,7 @@ function checkCodeMap(cwd) {
|
|
|
10709
11262
|
const more = probe.staleCount > 3 ? ` (+${probe.staleCount - 3} more)` : "";
|
|
10710
11263
|
findings.push({ level: "warning", message: `code-map stale: ${top}${more}; run \`cdd-kit code-map\`` });
|
|
10711
11264
|
}
|
|
10712
|
-
const text =
|
|
11265
|
+
const text = readFileSync30(mapPath, "utf8");
|
|
10713
11266
|
const m = text.match(/^# files: (\d+), src-lines: (\d+), map-lines: (\d+), compression: ([\d.]+)x/m);
|
|
10714
11267
|
if (m) {
|
|
10715
11268
|
findings.push({ level: "ok", message: `code-map: ${m[1]} files, ${m[4]}x compression` });
|
|
@@ -10718,6 +11271,68 @@ function checkCodeMap(cwd) {
|
|
|
10718
11271
|
}
|
|
10719
11272
|
return findings;
|
|
10720
11273
|
}
|
|
11274
|
+
function checkApiConformance(cwd) {
|
|
11275
|
+
const hasContract = existsSync26(join28(cwd, "contracts", "api", "api-contract.md"));
|
|
11276
|
+
if (!hasContract)
|
|
11277
|
+
return [];
|
|
11278
|
+
const configPath = join28(cwd, ".cdd", "conformance.json");
|
|
11279
|
+
if (!existsSync26(configPath)) {
|
|
11280
|
+
return [{
|
|
11281
|
+
level: "ok",
|
|
11282
|
+
message: 'API conformance: not configured (add .cdd/conformance.json with "enabled": true to catch frontend/backend drift against the contract)'
|
|
11283
|
+
}];
|
|
11284
|
+
}
|
|
11285
|
+
try {
|
|
11286
|
+
const cfg = JSON.parse(readFileSync30(configPath, "utf8"));
|
|
11287
|
+
return [{
|
|
11288
|
+
level: "ok",
|
|
11289
|
+
message: cfg.enabled ? "API conformance: enabled (cdd-kit validate --contracts checks code against the API contract)" : 'API conformance: present but disabled (set "enabled": true in .cdd/conformance.json to enforce code-vs-contract checks)'
|
|
11290
|
+
}];
|
|
11291
|
+
} catch {
|
|
11292
|
+
return [{ level: "warning", message: ".cdd/conformance.json is not valid JSON" }];
|
|
11293
|
+
}
|
|
11294
|
+
}
|
|
11295
|
+
function checkMcpRegistration(cwd, provider) {
|
|
11296
|
+
if (provider !== "claude" && provider !== "both")
|
|
11297
|
+
return [];
|
|
11298
|
+
if (!existsSync26(join28(cwd, ".cdd")))
|
|
11299
|
+
return [];
|
|
11300
|
+
const bin = process.env.CDD_CLAUDE_BIN || "claude";
|
|
11301
|
+
let result;
|
|
11302
|
+
try {
|
|
11303
|
+
result = bin.toLowerCase().endsWith(".js") ? spawnSync6(process.execPath, [bin, "mcp", "list"], { encoding: "utf8", timeout: 3e3 }) : spawnSync6(bin, ["mcp", "list"], { encoding: "utf8", timeout: 3e3 });
|
|
11304
|
+
} catch {
|
|
11305
|
+
result = { error: new Error("spawn failed") };
|
|
11306
|
+
}
|
|
11307
|
+
if (result.error || typeof result.status !== "number") {
|
|
11308
|
+
return [{
|
|
11309
|
+
level: "ok",
|
|
11310
|
+
message: "MCP: could not run `claude mcp list` (Claude Code CLI not found); skip if you do not use Claude Code"
|
|
11311
|
+
}];
|
|
11312
|
+
}
|
|
11313
|
+
if (result.status !== 0) {
|
|
11314
|
+
return [{
|
|
11315
|
+
level: "ok",
|
|
11316
|
+
message: `MCP: \`claude mcp list\` exited ${result.status}; cannot verify cdd-kit registration`
|
|
11317
|
+
}];
|
|
11318
|
+
}
|
|
11319
|
+
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
|
|
11320
|
+
if (/\bcdd-kit\b/.test(output)) {
|
|
11321
|
+
return [{ level: "ok", message: "MCP: cdd-kit server is registered with Claude Code" }];
|
|
11322
|
+
}
|
|
11323
|
+
return [{
|
|
11324
|
+
level: "ok",
|
|
11325
|
+
message: "MCP: cdd-kit not registered \u2014 agents will fall back to Read instead of graph/index tools; run `claude mcp add --scope user cdd-kit -- cdd-kit mcp`"
|
|
11326
|
+
}];
|
|
11327
|
+
}
|
|
11328
|
+
function checkChokepoints(cwd) {
|
|
11329
|
+
if (!existsSync26(join28(cwd, ".cdd")))
|
|
11330
|
+
return [];
|
|
11331
|
+
return detectChokepoints(cwd).map((c) => ({
|
|
11332
|
+
level: "ok",
|
|
11333
|
+
message: c.live ? `chokepoint ${c.name}: live \u2014 ${c.detail}` : `chokepoint ${c.name}: ${c.detail}`
|
|
11334
|
+
}));
|
|
11335
|
+
}
|
|
10721
11336
|
async function buildDoctorReport(cwd, opts) {
|
|
10722
11337
|
const requestedProvider = opts.provider ?? "auto";
|
|
10723
11338
|
if (!validateProviderOption(requestedProvider)) {
|
|
@@ -10743,6 +11358,9 @@ async function buildDoctorReport(cwd, opts) {
|
|
|
10743
11358
|
findings.push(...checkModelPolicyDrift(cwd));
|
|
10744
11359
|
findings.push(...checkAgentLint(cwd));
|
|
10745
11360
|
findings.push(...checkCodeMap(cwd));
|
|
11361
|
+
findings.push(...checkApiConformance(cwd));
|
|
11362
|
+
findings.push(...checkMcpRegistration(cwd, provider));
|
|
11363
|
+
findings.push(...checkChokepoints(cwd));
|
|
10746
11364
|
const errors = findings.filter((finding) => finding.level === "error").length;
|
|
10747
11365
|
const warnings = findings.filter((finding) => finding.level === "warning").length;
|
|
10748
11366
|
return {
|
|
@@ -10785,11 +11403,11 @@ async function attemptAutoFixes(cwd, report) {
|
|
|
10785
11403
|
}
|
|
10786
11404
|
}
|
|
10787
11405
|
if (/model-policy\.json has no role bindings/i.test(finding.message)) {
|
|
10788
|
-
const policyPath =
|
|
11406
|
+
const policyPath = join28(cwd, ".cdd", "model-policy.json");
|
|
10789
11407
|
try {
|
|
10790
11408
|
let existing = {};
|
|
10791
11409
|
try {
|
|
10792
|
-
existing = JSON.parse(
|
|
11410
|
+
existing = JSON.parse(readFileSync30(policyPath, "utf8"));
|
|
10793
11411
|
} catch {
|
|
10794
11412
|
}
|
|
10795
11413
|
const merged = {
|
|
@@ -10814,8 +11432,8 @@ async function attemptAutoFixes(cwd, report) {
|
|
|
10814
11432
|
"repo-context-scanner": "haiku"
|
|
10815
11433
|
}
|
|
10816
11434
|
};
|
|
10817
|
-
const { writeFileSync:
|
|
10818
|
-
|
|
11435
|
+
const { writeFileSync: writeFileSync19 } = await import("fs");
|
|
11436
|
+
writeFileSync19(policyPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
10819
11437
|
fixed.push(`populated .cdd/model-policy.json with default role bindings`);
|
|
10820
11438
|
continue;
|
|
10821
11439
|
} catch (err) {
|
|
@@ -10884,16 +11502,188 @@ var init_doctor = __esm({
|
|
|
10884
11502
|
init_provider();
|
|
10885
11503
|
init_freshness();
|
|
10886
11504
|
init_lint_agents();
|
|
11505
|
+
init_chokepoints();
|
|
10887
11506
|
init_digest();
|
|
10888
11507
|
}
|
|
10889
11508
|
});
|
|
10890
11509
|
|
|
10891
|
-
// src/commands/code-map-
|
|
10892
|
-
var
|
|
10893
|
-
__export(
|
|
10894
|
-
|
|
11510
|
+
// src/commands/code-map-watch.ts
|
|
11511
|
+
var code_map_watch_exports = {};
|
|
11512
|
+
__export(code_map_watch_exports, {
|
|
11513
|
+
codeMapWatch: () => codeMapWatch,
|
|
11514
|
+
createDebouncedRunner: () => createDebouncedRunner,
|
|
11515
|
+
generatedArtifactSet: () => generatedArtifactSet,
|
|
11516
|
+
makeWatchExcludeFilter: () => makeWatchExcludeFilter
|
|
10895
11517
|
});
|
|
10896
|
-
import {
|
|
11518
|
+
import { watch } from "fs";
|
|
11519
|
+
import { resolve as resolve5 } from "path";
|
|
11520
|
+
import picomatch2 from "picomatch";
|
|
11521
|
+
function createDebouncedRunner(run, debounceMs) {
|
|
11522
|
+
let timer = null;
|
|
11523
|
+
let running = false;
|
|
11524
|
+
let queued = false;
|
|
11525
|
+
async function fire() {
|
|
11526
|
+
if (running) {
|
|
11527
|
+
queued = true;
|
|
11528
|
+
return;
|
|
11529
|
+
}
|
|
11530
|
+
running = true;
|
|
11531
|
+
try {
|
|
11532
|
+
await run();
|
|
11533
|
+
} finally {
|
|
11534
|
+
running = false;
|
|
11535
|
+
if (queued) {
|
|
11536
|
+
queued = false;
|
|
11537
|
+
void fire();
|
|
11538
|
+
}
|
|
11539
|
+
}
|
|
11540
|
+
}
|
|
11541
|
+
function trigger() {
|
|
11542
|
+
if (timer)
|
|
11543
|
+
clearTimeout(timer);
|
|
11544
|
+
timer = setTimeout(() => {
|
|
11545
|
+
timer = null;
|
|
11546
|
+
void fire();
|
|
11547
|
+
}, debounceMs);
|
|
11548
|
+
}
|
|
11549
|
+
function dispose() {
|
|
11550
|
+
if (timer) {
|
|
11551
|
+
clearTimeout(timer);
|
|
11552
|
+
timer = null;
|
|
11553
|
+
}
|
|
11554
|
+
}
|
|
11555
|
+
return { trigger, dispose };
|
|
11556
|
+
}
|
|
11557
|
+
function generatedArtifactSet(outRel, cwd = process.cwd()) {
|
|
11558
|
+
return /* @__PURE__ */ new Set([
|
|
11559
|
+
resolve5(cwd, outRel),
|
|
11560
|
+
resolve5(cwd, sidecarPathFor(outRel)),
|
|
11561
|
+
resolve5(cwd, graphPathFor(outRel))
|
|
11562
|
+
]);
|
|
11563
|
+
}
|
|
11564
|
+
function makeWatchExcludeFilter(excludes, configRel = CONFIG_REL_PATH) {
|
|
11565
|
+
if (excludes.length === 0)
|
|
11566
|
+
return () => false;
|
|
11567
|
+
const match = picomatch2(excludes, { dot: true });
|
|
11568
|
+
return (rel) => rel !== configRel && (match(rel) || match(`${rel}/**`));
|
|
11569
|
+
}
|
|
11570
|
+
async function codeMapWatch(opts) {
|
|
11571
|
+
const debounceMs = opts.debounceMs ?? 500;
|
|
11572
|
+
const pollMs = opts.pollMs ?? 2e3;
|
|
11573
|
+
const scanPath = opts.surface ?? opts.path;
|
|
11574
|
+
const root = resolve5(process.cwd(), scanPath);
|
|
11575
|
+
const outRel = opts.out ?? (opts.surface ? `.cdd/code-map.${slugifySurface(opts.surface)}.yml` : ".cdd/code-map.yml");
|
|
11576
|
+
const generatedAbs = generatedArtifactSet(outRel);
|
|
11577
|
+
let isExcluded = () => false;
|
|
11578
|
+
try {
|
|
11579
|
+
const cfg = loadCodeMapConfig(root);
|
|
11580
|
+
isExcluded = makeWatchExcludeFilter([...cfg.exclude, ...opts.exclude ?? []]);
|
|
11581
|
+
} catch {
|
|
11582
|
+
}
|
|
11583
|
+
const rebuild = async () => {
|
|
11584
|
+
const exit = await codeMap({ ...opts, check: false, silent: true });
|
|
11585
|
+
if (exit === 0) {
|
|
11586
|
+
log.ok(`code-map refreshed (${(/* @__PURE__ */ new Date()).toLocaleTimeString()})`);
|
|
11587
|
+
} else {
|
|
11588
|
+
log.warn("code-map refresh reported a problem; map left unchanged where possible.");
|
|
11589
|
+
}
|
|
11590
|
+
return exit;
|
|
11591
|
+
};
|
|
11592
|
+
log.info(`code-map --watch: building initial map for ${scanPath}\u2026`);
|
|
11593
|
+
const initialExit = await rebuild();
|
|
11594
|
+
if (initialExit !== 0) {
|
|
11595
|
+
log.error("code-map --watch: initial build failed; not starting the watcher.");
|
|
11596
|
+
return initialExit;
|
|
11597
|
+
}
|
|
11598
|
+
const { trigger, dispose } = createDebouncedRunner(() => rebuild().then(() => void 0), debounceMs);
|
|
11599
|
+
let watcher = null;
|
|
11600
|
+
let pollTimer = null;
|
|
11601
|
+
let driftChecking = false;
|
|
11602
|
+
const triggerIfDrift = () => {
|
|
11603
|
+
if (driftChecking)
|
|
11604
|
+
return;
|
|
11605
|
+
driftChecking = true;
|
|
11606
|
+
void codeMap({ ...opts, check: true, silent: true }).then((drift) => {
|
|
11607
|
+
if (drift !== 0)
|
|
11608
|
+
trigger();
|
|
11609
|
+
}).finally(() => {
|
|
11610
|
+
driftChecking = false;
|
|
11611
|
+
});
|
|
11612
|
+
};
|
|
11613
|
+
const startPolling = (reason) => {
|
|
11614
|
+
if (pollTimer)
|
|
11615
|
+
return;
|
|
11616
|
+
if (reason)
|
|
11617
|
+
log.warn(reason);
|
|
11618
|
+
pollTimer = setInterval(triggerIfDrift, pollMs);
|
|
11619
|
+
log.ok(`polling ${scanPath} every ${pollMs}ms. Ctrl-C to stop.`);
|
|
11620
|
+
};
|
|
11621
|
+
const isSelfWrite = (filename) => {
|
|
11622
|
+
return generatedAbs.has(resolve5(root, filename));
|
|
11623
|
+
};
|
|
11624
|
+
try {
|
|
11625
|
+
watcher = watch(root, { recursive: true }, (_event, filename) => {
|
|
11626
|
+
if (filename) {
|
|
11627
|
+
const rel = filename.toString();
|
|
11628
|
+
if (isSelfWrite(rel))
|
|
11629
|
+
return;
|
|
11630
|
+
if (isExcluded(rel.replace(/\\/g, "/")))
|
|
11631
|
+
return;
|
|
11632
|
+
trigger();
|
|
11633
|
+
} else {
|
|
11634
|
+
triggerIfDrift();
|
|
11635
|
+
}
|
|
11636
|
+
});
|
|
11637
|
+
watcher.on("error", (err) => {
|
|
11638
|
+
log.warn(`watch error (${err.message}); closing watcher and falling back to polling.`);
|
|
11639
|
+
try {
|
|
11640
|
+
watcher?.close();
|
|
11641
|
+
} catch {
|
|
11642
|
+
}
|
|
11643
|
+
watcher = null;
|
|
11644
|
+
startPolling();
|
|
11645
|
+
});
|
|
11646
|
+
log.ok(`watching ${scanPath} (recursive, debounce ${debounceMs}ms). Ctrl-C to stop.`);
|
|
11647
|
+
} catch {
|
|
11648
|
+
startPolling("recursive fs.watch unavailable on this platform; falling back to freshness polling.");
|
|
11649
|
+
}
|
|
11650
|
+
return await new Promise((resolvePromise) => {
|
|
11651
|
+
const stop = () => {
|
|
11652
|
+
dispose();
|
|
11653
|
+
if (watcher)
|
|
11654
|
+
watcher.close();
|
|
11655
|
+
if (pollTimer)
|
|
11656
|
+
clearInterval(pollTimer);
|
|
11657
|
+
log.info("code-map --watch stopped.");
|
|
11658
|
+
resolvePromise(0);
|
|
11659
|
+
};
|
|
11660
|
+
const signal = opts.signal;
|
|
11661
|
+
if (signal) {
|
|
11662
|
+
if (signal.aborted) {
|
|
11663
|
+
stop();
|
|
11664
|
+
return;
|
|
11665
|
+
}
|
|
11666
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
11667
|
+
}
|
|
11668
|
+
});
|
|
11669
|
+
}
|
|
11670
|
+
var init_code_map_watch = __esm({
|
|
11671
|
+
"src/commands/code-map-watch.ts"() {
|
|
11672
|
+
"use strict";
|
|
11673
|
+
init_logger();
|
|
11674
|
+
init_code_map();
|
|
11675
|
+
init_config();
|
|
11676
|
+
init_index_reader();
|
|
11677
|
+
init_reader();
|
|
11678
|
+
}
|
|
11679
|
+
});
|
|
11680
|
+
|
|
11681
|
+
// src/commands/code-map-scan-worker.ts
|
|
11682
|
+
var code_map_scan_worker_exports = {};
|
|
11683
|
+
__export(code_map_scan_worker_exports, {
|
|
11684
|
+
runScanWorker: () => runScanWorker
|
|
11685
|
+
});
|
|
11686
|
+
import { readFileSync as readFileSync31 } from "fs";
|
|
10897
11687
|
async function runScanWorker(opts) {
|
|
10898
11688
|
let scanner;
|
|
10899
11689
|
switch (opts.lang) {
|
|
@@ -10913,7 +11703,7 @@ async function runScanWorker(opts) {
|
|
|
10913
11703
|
}
|
|
10914
11704
|
let files;
|
|
10915
11705
|
try {
|
|
10916
|
-
files =
|
|
11706
|
+
files = readFileSync31(opts.batchFile, "utf8").split("\n").map((s) => s.trim()).filter(Boolean);
|
|
10917
11707
|
} catch (err) {
|
|
10918
11708
|
process.stderr.write(`__code-map-scan: cannot read --batch-file: ${err.message}
|
|
10919
11709
|
`);
|
|
@@ -10933,10 +11723,16 @@ var init_code_map_scan_worker = __esm({
|
|
|
10933
11723
|
// src/commands/index-query.ts
|
|
10934
11724
|
var index_query_exports = {};
|
|
10935
11725
|
__export(index_query_exports, {
|
|
11726
|
+
DEFAULT_SOURCE_BUDGET: () => DEFAULT_SOURCE_BUDGET,
|
|
10936
11727
|
indexQuery: () => indexQuery,
|
|
10937
|
-
queryEntries: () => queryEntries
|
|
11728
|
+
queryEntries: () => queryEntries,
|
|
11729
|
+
resolveSourceBudget: () => resolveSourceBudget,
|
|
11730
|
+
surfaceRootFor: () => surfaceRootFor
|
|
10938
11731
|
});
|
|
10939
|
-
import { existsSync as
|
|
11732
|
+
import { existsSync as existsSync27, readFileSync as readFileSync32 } from "fs";
|
|
11733
|
+
function resolveSourceBudget(budget) {
|
|
11734
|
+
return Number.isFinite(budget) && budget > 0 ? Math.floor(budget) : DEFAULT_SOURCE_BUDGET;
|
|
11735
|
+
}
|
|
10940
11736
|
async function indexQuery(term, opts) {
|
|
10941
11737
|
const mapPath = opts.map || ".cdd/code-map.yml";
|
|
10942
11738
|
const limit = Number.isFinite(opts.limit) && opts.limit > 0 ? Math.floor(opts.limit) : 10;
|
|
@@ -10946,7 +11742,7 @@ async function indexQuery(term, opts) {
|
|
|
10946
11742
|
return printFailure(freshness.error, opts.json);
|
|
10947
11743
|
}
|
|
10948
11744
|
refreshed = freshness.refreshed;
|
|
10949
|
-
if (!
|
|
11745
|
+
if (!existsSync27(mapPath)) {
|
|
10950
11746
|
return printFailure(`${mapPath} is missing; run \`cdd-kit code-map\` first.`, opts.json);
|
|
10951
11747
|
}
|
|
10952
11748
|
let entries;
|
|
@@ -10956,6 +11752,9 @@ async function indexQuery(term, opts) {
|
|
|
10956
11752
|
return printFailure(`${mapPath} is not readable YAML: ${err.message}`, opts.json);
|
|
10957
11753
|
}
|
|
10958
11754
|
const results = queryEntries(entries, term).slice(0, limit);
|
|
11755
|
+
if (opts.withSource) {
|
|
11756
|
+
attachSource(results, resolveSourceBudget(opts.sourceBudget), surfaceRootFor(mapPath));
|
|
11757
|
+
}
|
|
10959
11758
|
const payload = {
|
|
10960
11759
|
index: mapPath,
|
|
10961
11760
|
query: term,
|
|
@@ -11074,6 +11873,68 @@ function firstLine(lines) {
|
|
|
11074
11873
|
const m = lines?.match(/^\d+/);
|
|
11075
11874
|
return m ? Number(m[0]) : Number.MAX_SAFE_INTEGER;
|
|
11076
11875
|
}
|
|
11876
|
+
function surfaceRootFor(mapPath) {
|
|
11877
|
+
try {
|
|
11878
|
+
if (!existsSync27(mapPath))
|
|
11879
|
+
return void 0;
|
|
11880
|
+
const head = readFileSync32(mapPath, "utf8").slice(0, 4096);
|
|
11881
|
+
const m = head.match(/^# surface-root:\s*(.+)$/m);
|
|
11882
|
+
return m ? m[1].trim() : void 0;
|
|
11883
|
+
} catch {
|
|
11884
|
+
return void 0;
|
|
11885
|
+
}
|
|
11886
|
+
}
|
|
11887
|
+
function attachSource(results, budget, baseDir) {
|
|
11888
|
+
const fileCache = /* @__PURE__ */ new Map();
|
|
11889
|
+
let used = 0;
|
|
11890
|
+
const readLines = (path) => {
|
|
11891
|
+
if (fileCache.has(path))
|
|
11892
|
+
return fileCache.get(path) ?? null;
|
|
11893
|
+
let lines = null;
|
|
11894
|
+
try {
|
|
11895
|
+
const resolved = baseDir && baseDir !== "." ? `${baseDir.replace(/\/+$/, "")}/${path}` : path;
|
|
11896
|
+
lines = existsSync27(resolved) ? readFileSync32(resolved, "utf8").split(/\r?\n/) : null;
|
|
11897
|
+
} catch {
|
|
11898
|
+
lines = null;
|
|
11899
|
+
}
|
|
11900
|
+
fileCache.set(path, lines);
|
|
11901
|
+
return lines;
|
|
11902
|
+
};
|
|
11903
|
+
for (const result of results) {
|
|
11904
|
+
const fileLines = readLines(result.path);
|
|
11905
|
+
if (!fileLines)
|
|
11906
|
+
continue;
|
|
11907
|
+
for (const match of result.matches) {
|
|
11908
|
+
const range = resolveRange(match);
|
|
11909
|
+
if (!range)
|
|
11910
|
+
continue;
|
|
11911
|
+
const [start, end] = range;
|
|
11912
|
+
if (used >= budget) {
|
|
11913
|
+
match.source_truncated = true;
|
|
11914
|
+
continue;
|
|
11915
|
+
}
|
|
11916
|
+
const allowedEnd = Math.min(end, start + (budget - used) - 1);
|
|
11917
|
+
const slice = fileLines.slice(start - 1, allowedEnd);
|
|
11918
|
+
match.source = slice.join("\n");
|
|
11919
|
+
used += slice.length;
|
|
11920
|
+
if (allowedEnd < end)
|
|
11921
|
+
match.source_truncated = true;
|
|
11922
|
+
}
|
|
11923
|
+
}
|
|
11924
|
+
}
|
|
11925
|
+
function resolveRange(match) {
|
|
11926
|
+
if (match.lines) {
|
|
11927
|
+
const m = match.lines.match(/^(\d+)\s*-\s*(\d+)$/);
|
|
11928
|
+
if (m)
|
|
11929
|
+
return [Number(m[1]), Number(m[2])];
|
|
11930
|
+
const single = match.lines.match(/^(\d+)$/);
|
|
11931
|
+
if (single)
|
|
11932
|
+
return [Number(single[1]), Number(single[1])];
|
|
11933
|
+
}
|
|
11934
|
+
if (typeof match.line === "number")
|
|
11935
|
+
return [match.line, match.line];
|
|
11936
|
+
return null;
|
|
11937
|
+
}
|
|
11077
11938
|
function printText(payload) {
|
|
11078
11939
|
if (payload.results.length === 0) {
|
|
11079
11940
|
console.log(`No matches for "${payload.query}" in ${payload.index}.`);
|
|
@@ -11089,9 +11950,18 @@ function printText(payload) {
|
|
|
11089
11950
|
const loc = match.lines ? ` lines ${match.lines}` : match.line ? ` line ${match.line}` : "";
|
|
11090
11951
|
const detail = match.detail && match.detail !== match.name ? ` - ${match.detail}` : "";
|
|
11091
11952
|
console.log(` - ${match.kind}: ${match.name}${loc}${detail}`);
|
|
11953
|
+
if (match.source !== void 0) {
|
|
11954
|
+
for (const srcLine of match.source.split("\n")) {
|
|
11955
|
+
console.log(` | ${srcLine}`);
|
|
11956
|
+
}
|
|
11957
|
+
if (match.source_truncated)
|
|
11958
|
+
console.log(" | \u2026 (source budget reached; Read the rest directly)");
|
|
11959
|
+
} else if (match.source_truncated) {
|
|
11960
|
+
console.log(" (source budget reached; Read this range directly)");
|
|
11961
|
+
}
|
|
11092
11962
|
}
|
|
11093
11963
|
}
|
|
11094
|
-
console.log("Next: read only the listed file/ranges first.");
|
|
11964
|
+
console.log(payload.results.some((r) => r.matches.some((m) => m.source !== void 0)) ? "Source included above \u2014 no separate Read needed for these ranges." : "Next: read only the listed file/ranges first.");
|
|
11095
11965
|
}
|
|
11096
11966
|
function printFailure(message, json) {
|
|
11097
11967
|
if (json) {
|
|
@@ -11102,10 +11972,12 @@ function printFailure(message, json) {
|
|
|
11102
11972
|
}
|
|
11103
11973
|
return 1;
|
|
11104
11974
|
}
|
|
11975
|
+
var DEFAULT_SOURCE_BUDGET;
|
|
11105
11976
|
var init_index_query = __esm({
|
|
11106
11977
|
"src/commands/index-query.ts"() {
|
|
11107
11978
|
"use strict";
|
|
11108
11979
|
init_index_reader();
|
|
11980
|
+
DEFAULT_SOURCE_BUDGET = 400;
|
|
11109
11981
|
}
|
|
11110
11982
|
});
|
|
11111
11983
|
|
|
@@ -11114,7 +11986,7 @@ var index_impact_exports = {};
|
|
|
11114
11986
|
__export(index_impact_exports, {
|
|
11115
11987
|
indexImpact: () => indexImpact
|
|
11116
11988
|
});
|
|
11117
|
-
import { existsSync as
|
|
11989
|
+
import { existsSync as existsSync28 } from "fs";
|
|
11118
11990
|
import { posix as posix2 } from "path";
|
|
11119
11991
|
async function indexImpact(term, opts) {
|
|
11120
11992
|
const mapPath = opts.map || ".cdd/code-map.yml";
|
|
@@ -11123,7 +11995,7 @@ async function indexImpact(term, opts) {
|
|
|
11123
11995
|
if (freshness.error) {
|
|
11124
11996
|
return printFailure2(freshness.error, opts.json);
|
|
11125
11997
|
}
|
|
11126
|
-
if (!
|
|
11998
|
+
if (!existsSync28(mapPath)) {
|
|
11127
11999
|
return printFailure2(`${mapPath} is missing; run \`cdd-kit code-map\` first.`, opts.json);
|
|
11128
12000
|
}
|
|
11129
12001
|
let entries;
|
|
@@ -11477,22 +12349,22 @@ __export(graph_exports, {
|
|
|
11477
12349
|
graphStatus: () => graphStatus,
|
|
11478
12350
|
graphSync: () => graphSync
|
|
11479
12351
|
});
|
|
11480
|
-
import { existsSync as
|
|
11481
|
-
import { join as
|
|
11482
|
-
import { spawnSync as
|
|
12352
|
+
import { existsSync as existsSync29, readFileSync as readFileSync33 } from "fs";
|
|
12353
|
+
import { join as join29 } from "path";
|
|
12354
|
+
import { spawnSync as spawnSync7 } from "child_process";
|
|
11483
12355
|
function codeGraphCommand() {
|
|
11484
12356
|
return process.env.CDD_CODEGRAPH_BIN || "codegraph";
|
|
11485
12357
|
}
|
|
11486
12358
|
function runCodeGraph(args, cwd = process.cwd()) {
|
|
11487
12359
|
const command = codeGraphCommand();
|
|
11488
12360
|
if (command.toLowerCase().endsWith(".js")) {
|
|
11489
|
-
return
|
|
12361
|
+
return spawnSync7(process.execPath, [command, ...args], { cwd, encoding: "buffer" });
|
|
11490
12362
|
}
|
|
11491
|
-
return
|
|
12363
|
+
return spawnSync7(command, args, { cwd, encoding: "buffer" });
|
|
11492
12364
|
}
|
|
11493
12365
|
function probeCodeGraph(cwd = process.cwd()) {
|
|
11494
12366
|
const command = codeGraphCommand();
|
|
11495
|
-
const initialized =
|
|
12367
|
+
const initialized = existsSync29(join29(cwd, ".codegraph"));
|
|
11496
12368
|
const version = runCodeGraph(["--version"], cwd);
|
|
11497
12369
|
if (version.error) {
|
|
11498
12370
|
return {
|
|
@@ -11562,7 +12434,7 @@ async function ensureNativeGraph(mapPath, refresh2) {
|
|
|
11562
12434
|
if (freshness.error)
|
|
11563
12435
|
return { graphPath: graphPathFor(mapPath), refreshed: freshness.refreshed, error: freshness.error };
|
|
11564
12436
|
const graphPath = graphPathFor(mapPath);
|
|
11565
|
-
if (!
|
|
12437
|
+
if (!existsSync29(graphPath) && refresh2) {
|
|
11566
12438
|
const { codeMap: codeMap2 } = await Promise.resolve().then(() => (init_code_map(), code_map_exports));
|
|
11567
12439
|
const exit = await codeMap2({
|
|
11568
12440
|
path: ".",
|
|
@@ -11595,7 +12467,7 @@ async function graphStatus(opts = {}) {
|
|
|
11595
12467
|
const graphPath = graphPathFor(mapPath);
|
|
11596
12468
|
const freshness = checkCodeMapFreshness(cwd, mapPath);
|
|
11597
12469
|
let graphStats;
|
|
11598
|
-
if (
|
|
12470
|
+
if (existsSync29(graphPath)) {
|
|
11599
12471
|
try {
|
|
11600
12472
|
const graph2 = loadCodeGraph(graphPath);
|
|
11601
12473
|
graphStats = { graph: graphPath, nodes: graph2.nodes.length, edges: graph2.edges.length, unresolved: graph2.unresolved.length };
|
|
@@ -11664,8 +12536,10 @@ async function graphQuery(term, opts) {
|
|
|
11664
12536
|
try {
|
|
11665
12537
|
const graph2 = loadCodeGraph(ensured.graphPath);
|
|
11666
12538
|
const results = searchGraph(graph2, term, opts.limit);
|
|
12539
|
+
const sources = opts.withSource ? collectNodeSources(results.map((r) => r.node), resolveSourceBudget(opts.sourceBudget), surfaceRootFor(mapPath)) : /* @__PURE__ */ new Map();
|
|
11667
12540
|
if (opts.json) {
|
|
11668
|
-
|
|
12541
|
+
const withSrc = opts.withSource ? results.map((r) => ({ ...r, source: sources.get(r.node.id)?.source, source_truncated: sources.get(r.node.id)?.truncated })) : results;
|
|
12542
|
+
writeJson({ engine: "native", graph: ensured.graphPath, query: term, refreshed: ensured.refreshed, results: withSrc });
|
|
11669
12543
|
} else {
|
|
11670
12544
|
console.log(`graph: ${ensured.graphPath}${ensured.refreshed ? " (refreshed)" : ""}`);
|
|
11671
12545
|
console.log(`query: ${term}`);
|
|
@@ -11674,8 +12548,15 @@ async function graphQuery(term, opts) {
|
|
|
11674
12548
|
const n = result.node;
|
|
11675
12549
|
console.log(`- ${n.kind}: ${n.qualified_name} lines ${n.start_line}-${n.end_line}`);
|
|
11676
12550
|
console.log(` edges: ${result.edges.incoming} incoming, ${result.edges.outgoing} outgoing`);
|
|
12551
|
+
const src = sources.get(n.id);
|
|
12552
|
+
if (src) {
|
|
12553
|
+
for (const srcLine of src.source.split("\n"))
|
|
12554
|
+
console.log(` | ${srcLine}`);
|
|
12555
|
+
if (src.truncated)
|
|
12556
|
+
console.log(" | \u2026 (source budget reached; Read the rest directly)");
|
|
12557
|
+
}
|
|
11677
12558
|
}
|
|
11678
|
-
console.log("Next: run cdd-kit graph impact <node/file/symbol> before editing.");
|
|
12559
|
+
console.log(opts.withSource ? "Source included above \u2014 no separate Read needed for these ranges." : "Next: run cdd-kit graph impact <node/file/symbol> before editing.");
|
|
11679
12560
|
}
|
|
11680
12561
|
return results.length === 0 ? 1 : 0;
|
|
11681
12562
|
} catch (err) {
|
|
@@ -11686,9 +12567,44 @@ async function graphQuery(term, opts) {
|
|
|
11686
12567
|
map: opts.map || ".cdd/code-map.yml",
|
|
11687
12568
|
limit: opts.limit,
|
|
11688
12569
|
json: opts.json === true,
|
|
11689
|
-
refresh: opts.refresh !== false
|
|
12570
|
+
refresh: opts.refresh !== false,
|
|
12571
|
+
withSource: opts.withSource === true,
|
|
12572
|
+
sourceBudget: resolveSourceBudget(opts.sourceBudget)
|
|
11690
12573
|
});
|
|
11691
12574
|
}
|
|
12575
|
+
function collectNodeSources(nodes, budget, baseDir) {
|
|
12576
|
+
const out = /* @__PURE__ */ new Map();
|
|
12577
|
+
const fileCache = /* @__PURE__ */ new Map();
|
|
12578
|
+
let used = 0;
|
|
12579
|
+
const readLines = (path) => {
|
|
12580
|
+
if (fileCache.has(path))
|
|
12581
|
+
return fileCache.get(path) ?? null;
|
|
12582
|
+
let lines = null;
|
|
12583
|
+
try {
|
|
12584
|
+
const resolved = baseDir && baseDir !== "." ? `${baseDir.replace(/\/+$/, "")}/${path}` : path;
|
|
12585
|
+
lines = existsSync29(resolved) ? readFileSync33(resolved, "utf8").split(/\r?\n/) : null;
|
|
12586
|
+
} catch {
|
|
12587
|
+
lines = null;
|
|
12588
|
+
}
|
|
12589
|
+
fileCache.set(path, lines);
|
|
12590
|
+
return lines;
|
|
12591
|
+
};
|
|
12592
|
+
for (const node of nodes) {
|
|
12593
|
+
const fileLines = readLines(node.file_path);
|
|
12594
|
+
if (!fileLines || !node.start_line)
|
|
12595
|
+
continue;
|
|
12596
|
+
if (used >= budget) {
|
|
12597
|
+
out.set(node.id, { source: "", truncated: true });
|
|
12598
|
+
continue;
|
|
12599
|
+
}
|
|
12600
|
+
const end = node.end_line && node.end_line >= node.start_line ? node.end_line : node.start_line;
|
|
12601
|
+
const allowedEnd = Math.min(end, node.start_line + (budget - used) - 1);
|
|
12602
|
+
const slice = fileLines.slice(node.start_line - 1, allowedEnd);
|
|
12603
|
+
out.set(node.id, { source: slice.join("\n"), truncated: allowedEnd < end });
|
|
12604
|
+
used += slice.length;
|
|
12605
|
+
}
|
|
12606
|
+
return out;
|
|
12607
|
+
}
|
|
11692
12608
|
async function graphImpact2(term, opts) {
|
|
11693
12609
|
const selected = selectEngine(opts);
|
|
11694
12610
|
if ("error" in selected)
|
|
@@ -11784,7 +12700,7 @@ async function graphContext2(task, opts) {
|
|
|
11784
12700
|
const freshness = await ensureCodeMapFresh(mapPath, opts.refresh !== false);
|
|
11785
12701
|
if (freshness.error)
|
|
11786
12702
|
return printEngineError(freshness.error, opts.json, selected.probe);
|
|
11787
|
-
if (!
|
|
12703
|
+
if (!existsSync29(mapPath))
|
|
11788
12704
|
return printEngineError(`${mapPath} is missing; run \`cdd-kit code-map\` first.`, opts.json, selected.probe);
|
|
11789
12705
|
let entries;
|
|
11790
12706
|
try {
|
|
@@ -11835,12 +12751,70 @@ var init_graph = __esm({
|
|
|
11835
12751
|
}
|
|
11836
12752
|
});
|
|
11837
12753
|
|
|
12754
|
+
// src/commands/classify-check.ts
|
|
12755
|
+
var classify_check_exports = {};
|
|
12756
|
+
__export(classify_check_exports, {
|
|
12757
|
+
classifyCheck: () => classifyCheck
|
|
12758
|
+
});
|
|
12759
|
+
import { existsSync as existsSync30, readFileSync as readFileSync34 } from "fs";
|
|
12760
|
+
import { join as join30 } from "path";
|
|
12761
|
+
async function classifyCheck(changeId, opts = {}) {
|
|
12762
|
+
const cwd = process.cwd();
|
|
12763
|
+
const policy = loadTierPolicy(cwd);
|
|
12764
|
+
let intent = opts.text ?? "";
|
|
12765
|
+
let source = "inline --text";
|
|
12766
|
+
let paths = [];
|
|
12767
|
+
if (!intent && changeId) {
|
|
12768
|
+
const requestPath = join30(cwd, "specs", "changes", changeId, "change-request.md");
|
|
12769
|
+
if (existsSync30(requestPath))
|
|
12770
|
+
intent = readFileSync34(requestPath, "utf8");
|
|
12771
|
+
paths = getTouchedPaths(cwd);
|
|
12772
|
+
source = `specs/changes/${changeId}/change-request.md + touched paths`;
|
|
12773
|
+
}
|
|
12774
|
+
const floor = computeTierFloor(intent, policy, { paths });
|
|
12775
|
+
if (opts.json) {
|
|
12776
|
+
console.log(JSON.stringify({
|
|
12777
|
+
enabled: policy.enabled,
|
|
12778
|
+
source,
|
|
12779
|
+
floorTier: floor.floorTier,
|
|
12780
|
+
label: floor.label,
|
|
12781
|
+
matched: floor.matched
|
|
12782
|
+
}, null, 2));
|
|
12783
|
+
return 0;
|
|
12784
|
+
}
|
|
12785
|
+
if (!policy.enabled) {
|
|
12786
|
+
log.info("tier-floor policy is disabled (.cdd/tier-policy.json enabled:false) \u2014 no floor enforced.");
|
|
12787
|
+
return 0;
|
|
12788
|
+
}
|
|
12789
|
+
if (!intent && paths.length === 0) {
|
|
12790
|
+
log.warn('no intent text or touched paths found to scan (pass --text "<description>" or a change-id with change-request.md).');
|
|
12791
|
+
return 0;
|
|
12792
|
+
}
|
|
12793
|
+
if (floor.floorTier === null) {
|
|
12794
|
+
log.ok("no sensitive surface matched \u2014 classifier may assign any tier on merit.");
|
|
12795
|
+
return 0;
|
|
12796
|
+
}
|
|
12797
|
+
log.warn(`mechanical tier floor: ${floor.floorTier} (or stricter)`);
|
|
12798
|
+
log.info(` reason: ${floor.label}`);
|
|
12799
|
+
log.info(` matched: ${floor.matched.join(", ")}`);
|
|
12800
|
+
log.info(` \u2192 change-classification.md must declare tier ${floor.floorTier} or stricter; the gate enforces this.`);
|
|
12801
|
+
return 0;
|
|
12802
|
+
}
|
|
12803
|
+
var init_classify_check = __esm({
|
|
12804
|
+
"src/commands/classify-check.ts"() {
|
|
12805
|
+
"use strict";
|
|
12806
|
+
init_logger();
|
|
12807
|
+
init_tier_floor();
|
|
12808
|
+
init_git_paths();
|
|
12809
|
+
}
|
|
12810
|
+
});
|
|
12811
|
+
|
|
11838
12812
|
// src/mcp/server.ts
|
|
11839
12813
|
var server_exports = {};
|
|
11840
12814
|
__export(server_exports, {
|
|
11841
12815
|
runMcpServer: () => runMcpServer
|
|
11842
12816
|
});
|
|
11843
|
-
import { spawnSync as
|
|
12817
|
+
import { spawnSync as spawnSync8 } from "child_process";
|
|
11844
12818
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
11845
12819
|
import { createInterface } from "readline";
|
|
11846
12820
|
async function runMcpServer(opts) {
|
|
@@ -11931,6 +12905,7 @@ function callTool(name, args) {
|
|
|
11931
12905
|
"--limit",
|
|
11932
12906
|
String(optionalInt(args.limit, 10)),
|
|
11933
12907
|
"--json",
|
|
12908
|
+
...sourceArgs(args),
|
|
11934
12909
|
...refreshArgs(args)
|
|
11935
12910
|
]);
|
|
11936
12911
|
case "cdd_graph_context":
|
|
@@ -11973,6 +12948,7 @@ function callTool(name, args) {
|
|
|
11973
12948
|
"--limit",
|
|
11974
12949
|
String(optionalInt(args.limit, 10)),
|
|
11975
12950
|
"--json",
|
|
12951
|
+
...sourceArgs(args),
|
|
11976
12952
|
...refreshArgs(args)
|
|
11977
12953
|
]);
|
|
11978
12954
|
case "cdd_index_impact":
|
|
@@ -11996,7 +12972,7 @@ function callTool(name, args) {
|
|
|
11996
12972
|
}
|
|
11997
12973
|
function runCddJson(args) {
|
|
11998
12974
|
const cliPath = process.argv[1] || fileURLToPath3(import.meta.url);
|
|
11999
|
-
const result =
|
|
12975
|
+
const result = spawnSync8(process.execPath, [cliPath, ...args], {
|
|
12000
12976
|
cwd: process.cwd(),
|
|
12001
12977
|
env: process.env,
|
|
12002
12978
|
encoding: "utf8"
|
|
@@ -12018,6 +12994,11 @@ function runCddJson(args) {
|
|
|
12018
12994
|
function refreshArgs(args) {
|
|
12019
12995
|
return args.refresh === false ? ["--no-refresh"] : [];
|
|
12020
12996
|
}
|
|
12997
|
+
function sourceArgs(args) {
|
|
12998
|
+
if (args.withSource !== true)
|
|
12999
|
+
return [];
|
|
13000
|
+
return ["--with-source", "--source-budget", String(optionalInt(args.sourceBudget, 400))];
|
|
13001
|
+
}
|
|
12021
13002
|
function asObject(value) {
|
|
12022
13003
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
12023
13004
|
}
|
|
@@ -12063,7 +13044,7 @@ var init_server = __esm({
|
|
|
12063
13044
|
},
|
|
12064
13045
|
{
|
|
12065
13046
|
name: "cdd_graph_query",
|
|
12066
|
-
description: "Search native graph symbols/files and return candidate nodes with line ranges.",
|
|
13047
|
+
description: "Search native graph symbols/files and return candidate nodes with line ranges. Set withSource:true to also return the source slices inline so no separate file read is needed.",
|
|
12067
13048
|
inputSchema: {
|
|
12068
13049
|
type: "object",
|
|
12069
13050
|
properties: {
|
|
@@ -12071,6 +13052,8 @@ var init_server = __esm({
|
|
|
12071
13052
|
limit: { type: "integer", minimum: 1, maximum: 100, default: 10 },
|
|
12072
13053
|
map: { type: "string", description: "Code-map YAML path.", default: DEFAULT_MAP },
|
|
12073
13054
|
engine: { type: "string", enum: ["auto", "native", "codegraph", "codemap"], default: "auto" },
|
|
13055
|
+
withSource: { type: "boolean", description: "Include matched source slices inline (replaces a follow-up read).", default: false },
|
|
13056
|
+
sourceBudget: { type: "integer", minimum: 1, maximum: 5e3, default: 400, description: "Max total source lines to return when withSource is true." },
|
|
12074
13057
|
refresh: { type: "boolean", default: true }
|
|
12075
13058
|
},
|
|
12076
13059
|
required: ["query"],
|
|
@@ -12112,13 +13095,15 @@ var init_server = __esm({
|
|
|
12112
13095
|
},
|
|
12113
13096
|
{
|
|
12114
13097
|
name: "cdd_index_query",
|
|
12115
|
-
description: "Fallback code-map query for files, symbols, imports, and line ranges.",
|
|
13098
|
+
description: "Fallback code-map query for files, symbols, imports, and line ranges. Set withSource:true to also return the source slices inline so no separate file read is needed.",
|
|
12116
13099
|
inputSchema: {
|
|
12117
13100
|
type: "object",
|
|
12118
13101
|
properties: {
|
|
12119
13102
|
query: { type: "string", description: "Symbol, file path, import module, enum member, or substring." },
|
|
12120
13103
|
limit: { type: "integer", minimum: 1, maximum: 100, default: 10 },
|
|
12121
13104
|
map: { type: "string", description: "Code-map YAML path.", default: DEFAULT_MAP },
|
|
13105
|
+
withSource: { type: "boolean", description: "Include matched source slices inline (replaces a follow-up read).", default: false },
|
|
13106
|
+
sourceBudget: { type: "integer", minimum: 1, maximum: 5e3, default: 400, description: "Max total source lines to return when withSource is true." },
|
|
12122
13107
|
refresh: { type: "boolean", default: true }
|
|
12123
13108
|
},
|
|
12124
13109
|
required: ["query"],
|
|
@@ -12149,28 +13134,28 @@ var archive_exports = {};
|
|
|
12149
13134
|
__export(archive_exports, {
|
|
12150
13135
|
archive: () => archive
|
|
12151
13136
|
});
|
|
12152
|
-
import { join as
|
|
12153
|
-
import { existsSync as
|
|
13137
|
+
import { join as join31 } from "path";
|
|
13138
|
+
import { existsSync as existsSync31, mkdirSync as mkdirSync12, renameSync as renameSync2, readFileSync as readFileSync35, writeFileSync as writeFileSync16, appendFileSync, cpSync as cpSync3, rmSync as rmSync3 } from "fs";
|
|
12154
13139
|
import yaml4 from "js-yaml";
|
|
12155
13140
|
async function archive(changeId) {
|
|
12156
13141
|
const cwd = process.cwd();
|
|
12157
|
-
const changeDir =
|
|
13142
|
+
const changeDir = join31(cwd, "specs", "changes", changeId);
|
|
12158
13143
|
const archiveYear = (/* @__PURE__ */ new Date()).getFullYear().toString();
|
|
12159
|
-
const archiveBase =
|
|
12160
|
-
const archiveDir =
|
|
12161
|
-
const indexPath =
|
|
12162
|
-
if (!
|
|
13144
|
+
const archiveBase = join31(cwd, "specs", "archive", archiveYear);
|
|
13145
|
+
const archiveDir = join31(archiveBase, changeId);
|
|
13146
|
+
const indexPath = join31(cwd, "specs", "archive", "INDEX.md");
|
|
13147
|
+
if (!existsSync31(changeDir)) {
|
|
12163
13148
|
log.error(`Change not found: specs/changes/${changeId}`);
|
|
12164
13149
|
process.exit(1);
|
|
12165
13150
|
}
|
|
12166
|
-
if (
|
|
13151
|
+
if (existsSync31(archiveDir)) {
|
|
12167
13152
|
log.error(`Already archived: specs/archive/${archiveYear}/${changeId}`);
|
|
12168
13153
|
process.exit(1);
|
|
12169
13154
|
}
|
|
12170
|
-
const tasksPath =
|
|
12171
|
-
if (
|
|
13155
|
+
const tasksPath = join31(changeDir, "tasks.yml");
|
|
13156
|
+
if (existsSync31(tasksPath)) {
|
|
12172
13157
|
try {
|
|
12173
|
-
const raw =
|
|
13158
|
+
const raw = readFileSync35(tasksPath, "utf8");
|
|
12174
13159
|
const data = yaml4.load(raw);
|
|
12175
13160
|
if (data?.status === "gate-blocked") {
|
|
12176
13161
|
log.warn("tasks.yml has status: gate-blocked \u2014 archiving anyway (change was paused).");
|
|
@@ -12183,8 +13168,8 @@ async function archive(changeId) {
|
|
|
12183
13168
|
log.warn("tasks.yml could not be parsed \u2014 archiving anyway.");
|
|
12184
13169
|
}
|
|
12185
13170
|
}
|
|
12186
|
-
if (!
|
|
12187
|
-
|
|
13171
|
+
if (!existsSync31(archiveBase)) {
|
|
13172
|
+
mkdirSync12(archiveBase, { recursive: true });
|
|
12188
13173
|
}
|
|
12189
13174
|
try {
|
|
12190
13175
|
renameSync2(changeDir, archiveDir);
|
|
@@ -12200,8 +13185,8 @@ async function archive(changeId) {
|
|
|
12200
13185
|
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
12201
13186
|
const indexLine = `| ${changeId} | ${archiveYear} | ${today} | specs/archive/${archiveYear}/${changeId}/ |
|
|
12202
13187
|
`;
|
|
12203
|
-
if (!
|
|
12204
|
-
|
|
13188
|
+
if (!existsSync31(indexPath)) {
|
|
13189
|
+
writeFileSync16(indexPath, `# Archive Index
|
|
12205
13190
|
|
|
12206
13191
|
| change-id | year | archived-date | path |
|
|
12207
13192
|
|---|---|---|---|
|
|
@@ -12225,37 +13210,37 @@ var abandon_exports = {};
|
|
|
12225
13210
|
__export(abandon_exports, {
|
|
12226
13211
|
abandon: () => abandon
|
|
12227
13212
|
});
|
|
12228
|
-
import { join as
|
|
12229
|
-
import { existsSync as
|
|
13213
|
+
import { join as join32 } from "path";
|
|
13214
|
+
import { existsSync as existsSync32, readFileSync as readFileSync36, writeFileSync as writeFileSync17, appendFileSync as appendFileSync2, mkdirSync as mkdirSync13 } from "fs";
|
|
12230
13215
|
import yaml5 from "js-yaml";
|
|
12231
13216
|
async function abandon(changeId, opts) {
|
|
12232
13217
|
const cwd = process.cwd();
|
|
12233
|
-
const changeDir =
|
|
12234
|
-
const tasksPath =
|
|
12235
|
-
if (!
|
|
13218
|
+
const changeDir = join32(cwd, "specs", "changes", changeId);
|
|
13219
|
+
const tasksPath = join32(changeDir, "tasks.yml");
|
|
13220
|
+
if (!existsSync32(changeDir)) {
|
|
12236
13221
|
log.error(`Change not found: specs/changes/${changeId}`);
|
|
12237
13222
|
process.exit(1);
|
|
12238
13223
|
}
|
|
12239
|
-
if (
|
|
12240
|
-
const raw =
|
|
13224
|
+
if (existsSync32(tasksPath)) {
|
|
13225
|
+
const raw = readFileSync36(tasksPath, "utf8");
|
|
12241
13226
|
const data = yaml5.load(raw) ?? {};
|
|
12242
13227
|
data["status"] = "abandoned";
|
|
12243
13228
|
if (!data["change-id"]) {
|
|
12244
13229
|
data["change-id"] = changeId;
|
|
12245
13230
|
}
|
|
12246
|
-
|
|
13231
|
+
writeFileSync17(tasksPath, yaml5.dump(data, { lineWidth: -1, noRefs: true }), "utf8");
|
|
12247
13232
|
}
|
|
12248
13233
|
const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
12249
|
-
const archiveDir =
|
|
12250
|
-
const indexPath =
|
|
13234
|
+
const archiveDir = join32(cwd, "specs", "archive");
|
|
13235
|
+
const indexPath = join32(archiveDir, "INDEX.md");
|
|
12251
13236
|
const reason = opts.reason ?? "no reason given";
|
|
12252
13237
|
const indexLine = `| ${changeId} | abandoned | ${today} | ${reason} |
|
|
12253
13238
|
`;
|
|
12254
|
-
if (!
|
|
12255
|
-
|
|
13239
|
+
if (!existsSync32(archiveDir)) {
|
|
13240
|
+
mkdirSync13(archiveDir, { recursive: true });
|
|
12256
13241
|
}
|
|
12257
|
-
if (!
|
|
12258
|
-
|
|
13242
|
+
if (!existsSync32(indexPath)) {
|
|
13243
|
+
writeFileSync17(indexPath, `# Archive Index
|
|
12259
13244
|
|
|
12260
13245
|
| change-id | status | date | notes |
|
|
12261
13246
|
|---|---|---|---|
|
|
@@ -12279,28 +13264,28 @@ var list_changes_exports = {};
|
|
|
12279
13264
|
__export(list_changes_exports, {
|
|
12280
13265
|
listChanges: () => listChanges
|
|
12281
13266
|
});
|
|
12282
|
-
import { join as
|
|
12283
|
-
import { existsSync as
|
|
13267
|
+
import { join as join33 } from "path";
|
|
13268
|
+
import { existsSync as existsSync33, readdirSync as readdirSync14, readFileSync as readFileSync37 } from "fs";
|
|
12284
13269
|
import yaml6 from "js-yaml";
|
|
12285
13270
|
async function listChanges() {
|
|
12286
13271
|
const cwd = process.cwd();
|
|
12287
|
-
const changesDir =
|
|
13272
|
+
const changesDir = join33(cwd, "specs", "changes");
|
|
12288
13273
|
log.blank();
|
|
12289
13274
|
const active = [];
|
|
12290
|
-
if (
|
|
12291
|
-
active.push(...
|
|
13275
|
+
if (existsSync33(changesDir)) {
|
|
13276
|
+
active.push(...readdirSync14(changesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name));
|
|
12292
13277
|
}
|
|
12293
13278
|
if (active.length === 0) {
|
|
12294
13279
|
log.info("No active changes in specs/changes/");
|
|
12295
13280
|
} else {
|
|
12296
13281
|
log.info("Active changes:");
|
|
12297
13282
|
for (const id of active) {
|
|
12298
|
-
const tasksPath =
|
|
13283
|
+
const tasksPath = join33(changesDir, id, "tasks.yml");
|
|
12299
13284
|
let status = "in-progress";
|
|
12300
13285
|
let pending = 0;
|
|
12301
|
-
if (
|
|
13286
|
+
if (existsSync33(tasksPath)) {
|
|
12302
13287
|
try {
|
|
12303
|
-
const raw =
|
|
13288
|
+
const raw = readFileSync37(tasksPath, "utf8");
|
|
12304
13289
|
const data = yaml6.load(raw);
|
|
12305
13290
|
if (data?.status)
|
|
12306
13291
|
status = data.status;
|
|
@@ -12332,9 +13317,9 @@ __export(context_exports, {
|
|
|
12332
13317
|
rejectContextExpansion: () => rejectContextExpansion,
|
|
12333
13318
|
requestContextExpansion: () => requestContextExpansion
|
|
12334
13319
|
});
|
|
12335
|
-
import { existsSync as
|
|
12336
|
-
import { join as
|
|
12337
|
-
import
|
|
13320
|
+
import { existsSync as existsSync34, readFileSync as readFileSync38, writeFileSync as writeFileSync18 } from "fs";
|
|
13321
|
+
import { join as join34 } from "path";
|
|
13322
|
+
import picomatch3 from "picomatch";
|
|
12338
13323
|
function normalizePath(path) {
|
|
12339
13324
|
return path.replace(/\\/g, "/").replace(/^\.\//, "").trim();
|
|
12340
13325
|
}
|
|
@@ -12348,18 +13333,18 @@ function validateRepoRelativePath(path) {
|
|
|
12348
13333
|
return null;
|
|
12349
13334
|
}
|
|
12350
13335
|
function manifestPathFor(changeId) {
|
|
12351
|
-
return
|
|
13336
|
+
return join34(process.cwd(), "specs", "changes", changeId, "context-manifest.md");
|
|
12352
13337
|
}
|
|
12353
13338
|
function readManifest(changeId) {
|
|
12354
13339
|
const manifestPath = manifestPathFor(changeId);
|
|
12355
|
-
if (!
|
|
13340
|
+
if (!existsSync34(manifestPath)) {
|
|
12356
13341
|
log.error(`context manifest not found: specs/changes/${changeId}/context-manifest.md`);
|
|
12357
13342
|
process.exit(1);
|
|
12358
13343
|
}
|
|
12359
|
-
return
|
|
13344
|
+
return readFileSync38(manifestPath, "utf8");
|
|
12360
13345
|
}
|
|
12361
13346
|
function writeManifest(changeId, content) {
|
|
12362
|
-
|
|
13347
|
+
writeFileSync18(manifestPathFor(changeId), content.endsWith("\n") ? content : `${content}
|
|
12363
13348
|
`, "utf8");
|
|
12364
13349
|
}
|
|
12365
13350
|
function sectionBody(content, heading) {
|
|
@@ -12385,7 +13370,7 @@ function pathMatches(relPath, patterns, currentChangeId) {
|
|
|
12385
13370
|
return normalized.startsWith("specs/changes/");
|
|
12386
13371
|
}
|
|
12387
13372
|
if (/[*?[{]/.test(pattern)) {
|
|
12388
|
-
if (
|
|
13373
|
+
if (picomatch3.isMatch(normalized, pattern, { dot: true, nocase: false }))
|
|
12389
13374
|
return true;
|
|
12390
13375
|
if (pattern.endsWith("/**")) {
|
|
12391
13376
|
const base = pattern.slice(0, -3);
|
|
@@ -12398,11 +13383,11 @@ function pathMatches(relPath, patterns, currentChangeId) {
|
|
|
12398
13383
|
});
|
|
12399
13384
|
}
|
|
12400
13385
|
function loadContextPolicy() {
|
|
12401
|
-
const policyPath =
|
|
12402
|
-
if (!
|
|
13386
|
+
const policyPath = join34(process.cwd(), ".cdd", "context-policy.json");
|
|
13387
|
+
if (!existsSync34(policyPath))
|
|
12403
13388
|
return { forbiddenPaths: DEFAULT_FORBIDDEN_PATHS };
|
|
12404
13389
|
try {
|
|
12405
|
-
const custom = JSON.parse(
|
|
13390
|
+
const custom = JSON.parse(readFileSync38(policyPath, "utf8"));
|
|
12406
13391
|
return {
|
|
12407
13392
|
forbiddenPaths: Array.from(/* @__PURE__ */ new Set([
|
|
12408
13393
|
...DEFAULT_FORBIDDEN_PATHS,
|
|
@@ -12684,16 +13669,16 @@ var init_context = __esm({
|
|
|
12684
13669
|
});
|
|
12685
13670
|
|
|
12686
13671
|
// src/cli/index.ts
|
|
12687
|
-
import { readFileSync as
|
|
13672
|
+
import { readFileSync as readFileSync39 } from "fs";
|
|
12688
13673
|
import os from "os";
|
|
12689
13674
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
12690
|
-
import { dirname as
|
|
13675
|
+
import { dirname as dirname8, join as join35 } from "path";
|
|
12691
13676
|
import { Command } from "commander";
|
|
12692
13677
|
|
|
12693
13678
|
// src/commands/init.ts
|
|
12694
13679
|
init_paths();
|
|
12695
|
-
import { join as
|
|
12696
|
-
import { rmSync, readFileSync as
|
|
13680
|
+
import { join as join8 } from "path";
|
|
13681
|
+
import { rmSync, readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync7, readdirSync as readdirSync2 } from "fs";
|
|
12697
13682
|
|
|
12698
13683
|
// src/utils/copy.ts
|
|
12699
13684
|
init_logger();
|
|
@@ -12861,14 +13846,57 @@ function detectStack(repoRoot) {
|
|
|
12861
13846
|
};
|
|
12862
13847
|
}
|
|
12863
13848
|
|
|
13849
|
+
// src/commands/suggest-codegen.ts
|
|
13850
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync } from "fs";
|
|
13851
|
+
import { join as join4 } from "path";
|
|
13852
|
+
var ARTIFACT_PATH = "contracts/api/openapi.json";
|
|
13853
|
+
var CLIENT_OUT = "src/api/types.ts";
|
|
13854
|
+
var SCRIPT_GENERATE = "contract:client";
|
|
13855
|
+
var SCRIPT_CHECK = "contract:client:check";
|
|
13856
|
+
var GENERATE_CMD = `cdd-kit openapi export --out ${ARTIFACT_PATH} && npx --yes openapi-typescript ${ARTIFACT_PATH} -o ${CLIENT_OUT}`;
|
|
13857
|
+
var CHECK_CMD = `cdd-kit openapi export --check --out ${ARTIFACT_PATH}`;
|
|
13858
|
+
function suggestCodegenScript(cwd) {
|
|
13859
|
+
const pkgPath = join4(cwd, "package.json");
|
|
13860
|
+
if (!existsSync3(pkgPath)) {
|
|
13861
|
+
return { added: [], skipped: "no package.json (client codegen is consumer-specific; see docs/openapi-export.md)" };
|
|
13862
|
+
}
|
|
13863
|
+
let pkg2;
|
|
13864
|
+
let raw;
|
|
13865
|
+
try {
|
|
13866
|
+
raw = readFileSync2(pkgPath, "utf8");
|
|
13867
|
+
pkg2 = JSON.parse(raw);
|
|
13868
|
+
} catch {
|
|
13869
|
+
return { added: [], skipped: "package.json is not valid JSON; left untouched" };
|
|
13870
|
+
}
|
|
13871
|
+
if (typeof pkg2 !== "object" || pkg2 === null || Array.isArray(pkg2)) {
|
|
13872
|
+
return { added: [], skipped: "package.json is not a JSON object; left untouched" };
|
|
13873
|
+
}
|
|
13874
|
+
const scripts = typeof pkg2.scripts === "object" && pkg2.scripts !== null && !Array.isArray(pkg2.scripts) ? pkg2.scripts : {};
|
|
13875
|
+
const hasGenerate = SCRIPT_GENERATE in scripts;
|
|
13876
|
+
const hasCheck = SCRIPT_CHECK in scripts;
|
|
13877
|
+
if (hasGenerate || hasCheck) {
|
|
13878
|
+
const present = [hasGenerate ? SCRIPT_GENERATE : "", hasCheck ? SCRIPT_CHECK : ""].filter(Boolean).join(" + ");
|
|
13879
|
+
return { added: [], skipped: `${present} already present in package.json; leaving codegen wiring to you` };
|
|
13880
|
+
}
|
|
13881
|
+
scripts[SCRIPT_GENERATE] = GENERATE_CMD;
|
|
13882
|
+
scripts[SCRIPT_CHECK] = CHECK_CMD;
|
|
13883
|
+
pkg2.scripts = scripts;
|
|
13884
|
+
const trailing = raw.endsWith("\n") ? "\n" : "";
|
|
13885
|
+
writeFileSync(pkgPath, JSON.stringify(pkg2, null, 2) + trailing, "utf8");
|
|
13886
|
+
return {
|
|
13887
|
+
added: [SCRIPT_GENERATE, SCRIPT_CHECK],
|
|
13888
|
+
note: `requires openapi-typescript (\`npm i -D openapi-typescript\`); edit the output path (${CLIENT_OUT}) to fit your project, then wire \`npm run ${SCRIPT_CHECK}\` into CI`
|
|
13889
|
+
};
|
|
13890
|
+
}
|
|
13891
|
+
|
|
12864
13892
|
// src/commands/init.ts
|
|
12865
13893
|
init_mcp_hint();
|
|
12866
13894
|
function readCondaEnvName(cwd) {
|
|
12867
|
-
const envYml =
|
|
12868
|
-
if (!
|
|
13895
|
+
const envYml = join8(cwd, "environment.yml");
|
|
13896
|
+
if (!existsSync7(envYml))
|
|
12869
13897
|
return "base";
|
|
12870
13898
|
try {
|
|
12871
|
-
const content =
|
|
13899
|
+
const content = readFileSync6(envYml, "utf8");
|
|
12872
13900
|
const match = content.match(/^name:\s*(.+)$/m);
|
|
12873
13901
|
return match ? match[1].trim() : "base";
|
|
12874
13902
|
} catch {
|
|
@@ -12876,11 +13904,11 @@ function readCondaEnvName(cwd) {
|
|
|
12876
13904
|
}
|
|
12877
13905
|
}
|
|
12878
13906
|
function loadCiTemplate(stack) {
|
|
12879
|
-
const templatePath =
|
|
12880
|
-
if (!
|
|
13907
|
+
const templatePath = join8(ASSETS_DIR, "ci-templates", `${stack}.yml`);
|
|
13908
|
+
if (!existsSync7(templatePath))
|
|
12881
13909
|
return null;
|
|
12882
13910
|
try {
|
|
12883
|
-
return
|
|
13911
|
+
return readFileSync6(templatePath, "utf8");
|
|
12884
13912
|
} catch {
|
|
12885
13913
|
return null;
|
|
12886
13914
|
}
|
|
@@ -12916,6 +13944,7 @@ async function init(opts) {
|
|
|
12916
13944
|
}
|
|
12917
13945
|
const cwd = process.cwd();
|
|
12918
13946
|
const createdPaths = [];
|
|
13947
|
+
const restoreActions = [];
|
|
12919
13948
|
const installClaude = opts.provider === "claude" || opts.provider === "both";
|
|
12920
13949
|
const installCodex = opts.provider === "codex" || opts.provider === "both";
|
|
12921
13950
|
function track(paths) {
|
|
@@ -12931,6 +13960,13 @@ async function init(opts) {
|
|
|
12931
13960
|
log.warn(`could not remove: ${p}`);
|
|
12932
13961
|
}
|
|
12933
13962
|
}
|
|
13963
|
+
for (const restore of [...restoreActions].reverse()) {
|
|
13964
|
+
try {
|
|
13965
|
+
restore();
|
|
13966
|
+
} catch {
|
|
13967
|
+
log.warn("could not restore an in-place file edit");
|
|
13968
|
+
}
|
|
13969
|
+
}
|
|
12934
13970
|
}
|
|
12935
13971
|
log.blank();
|
|
12936
13972
|
log.info("Initialising contract-driven-delivery kit\u2026");
|
|
@@ -12944,9 +13980,9 @@ async function init(opts) {
|
|
|
12944
13980
|
const skillDirs = readdirSync2(ASSET.skills, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
12945
13981
|
let totalSkillFiles = 0;
|
|
12946
13982
|
for (const skillName of skillDirs) {
|
|
12947
|
-
const skillDest =
|
|
13983
|
+
const skillDest = join8(SKILLS_HOME, skillName);
|
|
12948
13984
|
log.info(`Installing skill \u2192 ${skillDest}`);
|
|
12949
|
-
const { count, created } = copyDirTracked(
|
|
13985
|
+
const { count, created } = copyDirTracked(join8(ASSET.skills, skillName), skillDest, { overwrite: true });
|
|
12950
13986
|
track(created);
|
|
12951
13987
|
totalSkillFiles += count;
|
|
12952
13988
|
}
|
|
@@ -12960,44 +13996,44 @@ async function init(opts) {
|
|
|
12960
13996
|
log.info(`Scaffolding project files in ${cwd}`);
|
|
12961
13997
|
const { count: contractsCount, created: contractsCreated } = copyDirTracked(
|
|
12962
13998
|
ASSET.contracts,
|
|
12963
|
-
|
|
13999
|
+
join8(cwd, "contracts"),
|
|
12964
14000
|
{ overwrite: opts.force, label: "contracts" }
|
|
12965
14001
|
);
|
|
12966
14002
|
track(contractsCreated);
|
|
12967
14003
|
log.ok(`contracts/ \u2014 ${contractsCount} file(s) written.`);
|
|
12968
14004
|
const { count: specsCount, created: specsCreated } = copyDirTracked(
|
|
12969
14005
|
ASSET.specsTemplates,
|
|
12970
|
-
|
|
14006
|
+
join8(cwd, "specs", "templates"),
|
|
12971
14007
|
{ overwrite: opts.force, label: "specs/templates" }
|
|
12972
14008
|
);
|
|
12973
14009
|
track(specsCreated);
|
|
12974
14010
|
log.ok(`specs/templates/ \u2014 ${specsCount} file(s) written.`);
|
|
12975
14011
|
const { count: testsCount, created: testsCreated } = copyDirTracked(
|
|
12976
14012
|
ASSET.testsTemplates,
|
|
12977
|
-
|
|
14013
|
+
join8(cwd, "tests", "templates"),
|
|
12978
14014
|
{ overwrite: opts.force, label: "tests/templates" }
|
|
12979
14015
|
);
|
|
12980
14016
|
track(testsCreated);
|
|
12981
14017
|
log.ok(`tests/templates/ \u2014 ${testsCount} file(s) written.`);
|
|
12982
14018
|
const { count: ciCount, created: ciCreated } = copyDirTracked(
|
|
12983
14019
|
ASSET.ci,
|
|
12984
|
-
|
|
14020
|
+
join8(cwd, "ci"),
|
|
12985
14021
|
{ overwrite: opts.force, label: "ci" }
|
|
12986
14022
|
);
|
|
12987
14023
|
track(ciCreated);
|
|
12988
14024
|
log.ok(`ci/ \u2014 ${ciCount} file(s) written.`);
|
|
12989
14025
|
const { count: cddConfigCount, created: cddConfigCreated } = copyDirTracked(
|
|
12990
14026
|
ASSET.cddConfig,
|
|
12991
|
-
|
|
14027
|
+
join8(cwd, ".cdd"),
|
|
12992
14028
|
{ overwrite: opts.force, label: ".cdd" }
|
|
12993
14029
|
);
|
|
12994
14030
|
track(cddConfigCreated);
|
|
12995
14031
|
log.ok(`.cdd/ - ${cddConfigCount} file(s) written.`);
|
|
12996
|
-
const modelPolicyPath =
|
|
12997
|
-
if (
|
|
14032
|
+
const modelPolicyPath = join8(cwd, ".cdd", "model-policy.json");
|
|
14033
|
+
if (existsSync7(modelPolicyPath)) {
|
|
12998
14034
|
let existing = {};
|
|
12999
14035
|
try {
|
|
13000
|
-
existing = JSON.parse(
|
|
14036
|
+
existing = JSON.parse(readFileSync6(modelPolicyPath, "utf8"));
|
|
13001
14037
|
} catch {
|
|
13002
14038
|
}
|
|
13003
14039
|
const merged = {
|
|
@@ -13006,11 +14042,11 @@ async function init(opts) {
|
|
|
13006
14042
|
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13007
14043
|
roles: existing.roles && typeof existing.roles === "object" && Object.keys(existing.roles).length > 0 ? existing.roles : {}
|
|
13008
14044
|
};
|
|
13009
|
-
|
|
14045
|
+
writeFileSync5(modelPolicyPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
13010
14046
|
}
|
|
13011
14047
|
const { count: wfCount, created: wfCreated } = copyDirTracked(
|
|
13012
14048
|
ASSET.githubWorkflows,
|
|
13013
|
-
|
|
14049
|
+
join8(cwd, ".github", "workflows"),
|
|
13014
14050
|
{ overwrite: opts.force, label: ".github/workflows" }
|
|
13015
14051
|
);
|
|
13016
14052
|
track(wfCreated);
|
|
@@ -13034,11 +14070,11 @@ async function init(opts) {
|
|
|
13034
14070
|
} else {
|
|
13035
14071
|
log.warn("Could not detect stack \u2014 CI placeholder left in place.");
|
|
13036
14072
|
}
|
|
13037
|
-
const ciYmlDest =
|
|
13038
|
-
if (
|
|
14073
|
+
const ciYmlDest = join8(cwd, ".github", "workflows", "contract-driven-gates.yml");
|
|
14074
|
+
if (existsSync7(ciYmlDest)) {
|
|
13039
14075
|
const template = loadCiTemplate(detection.primary);
|
|
13040
14076
|
if (template) {
|
|
13041
|
-
const original =
|
|
14077
|
+
const original = readFileSync6(ciYmlDest, "utf8");
|
|
13042
14078
|
let patched = patchFastGateYml(original, template, detection.primary);
|
|
13043
14079
|
if (detection.primary === "conda" && patched.includes("{{conda-env-name}}")) {
|
|
13044
14080
|
const envName = readCondaEnvName(cwd);
|
|
@@ -13046,39 +14082,52 @@ async function init(opts) {
|
|
|
13046
14082
|
log.ok(`Conda environment name set to: ${envName}`);
|
|
13047
14083
|
}
|
|
13048
14084
|
if (patched !== original) {
|
|
13049
|
-
|
|
14085
|
+
writeFileSync5(ciYmlDest, patched, "utf8");
|
|
13050
14086
|
log.ok(`CI fast-gate patched for stack: ${detection.primary}`);
|
|
13051
14087
|
}
|
|
13052
14088
|
}
|
|
13053
14089
|
}
|
|
14090
|
+
const pkgJsonPath = join8(cwd, "package.json");
|
|
14091
|
+
const pkgJsonBefore = existsSync7(pkgJsonPath) ? readFileSync6(pkgJsonPath, "utf8") : null;
|
|
14092
|
+
const codegen = suggestCodegenScript(cwd);
|
|
14093
|
+
if (codegen.added.length > 0) {
|
|
14094
|
+
if (pkgJsonBefore !== null) {
|
|
14095
|
+
restoreActions.push(() => writeFileSync5(pkgJsonPath, pkgJsonBefore, "utf8"));
|
|
14096
|
+
}
|
|
14097
|
+
log.ok(`package.json: added ${codegen.added.join(" + ")} script(s) for contract\u2192client codegen`);
|
|
14098
|
+
if (codegen.note)
|
|
14099
|
+
log.info(codegen.note);
|
|
14100
|
+
} else if (codegen.skipped && existsSync7(pkgJsonPath)) {
|
|
14101
|
+
log.info(`contract\u2192client codegen: ${codegen.skipped}`);
|
|
14102
|
+
}
|
|
13054
14103
|
if (installClaude) {
|
|
13055
14104
|
const { written: claudeWritten, created: claudeCreated } = copyFileTracked(
|
|
13056
14105
|
ASSET.claudeTemplate,
|
|
13057
|
-
|
|
14106
|
+
join8(cwd, "CLAUDE.md"),
|
|
13058
14107
|
{ overwrite: false, label: "CLAUDE.md" }
|
|
13059
14108
|
);
|
|
13060
14109
|
if (claudeCreated)
|
|
13061
|
-
track([
|
|
14110
|
+
track([join8(cwd, "CLAUDE.md")]);
|
|
13062
14111
|
if (claudeWritten)
|
|
13063
14112
|
log.ok("CLAUDE.md created.");
|
|
13064
14113
|
const { written: agentsWritten, created: agentsCreated } = copyFileTracked(
|
|
13065
14114
|
ASSET.agentsTemplate,
|
|
13066
|
-
|
|
14115
|
+
join8(cwd, "AGENTS.md"),
|
|
13067
14116
|
{ overwrite: false, label: "AGENTS.md" }
|
|
13068
14117
|
);
|
|
13069
14118
|
if (agentsCreated)
|
|
13070
|
-
track([
|
|
14119
|
+
track([join8(cwd, "AGENTS.md")]);
|
|
13071
14120
|
if (agentsWritten)
|
|
13072
14121
|
log.ok("AGENTS.md created.");
|
|
13073
14122
|
}
|
|
13074
14123
|
if (installCodex) {
|
|
13075
14124
|
const { written: codexWritten, created: codexCreated } = copyFileTracked(
|
|
13076
14125
|
ASSET.codexTemplate,
|
|
13077
|
-
|
|
14126
|
+
join8(cwd, "CODEX.md"),
|
|
13078
14127
|
{ overwrite: false, label: "CODEX.md" }
|
|
13079
14128
|
);
|
|
13080
14129
|
if (codexCreated)
|
|
13081
|
-
track([
|
|
14130
|
+
track([join8(cwd, "CODEX.md")]);
|
|
13082
14131
|
if (codexWritten)
|
|
13083
14132
|
log.ok("CODEX.md created.");
|
|
13084
14133
|
}
|
|
@@ -13086,6 +14135,16 @@ async function init(opts) {
|
|
|
13086
14135
|
const { installCodeMapHook: installCodeMapHook2 } = await Promise.resolve().then(() => (init_code_map_hook(), code_map_hook_exports));
|
|
13087
14136
|
await installCodeMapHook2(cwd);
|
|
13088
14137
|
}
|
|
14138
|
+
if (!opts.globalOnly && opts.arm !== false) {
|
|
14139
|
+
log.info("Arming enforcement chokepoints\u2026");
|
|
14140
|
+
if (installClaude) {
|
|
14141
|
+
const { installAgentHooks: installAgentHooks2 } = await Promise.resolve().then(() => (init_install_agent_hooks(), install_agent_hooks_exports));
|
|
14142
|
+
await installAgentHooks2({ graphFirst: "advisory", fromInit: true });
|
|
14143
|
+
}
|
|
14144
|
+
const { installHooks: installHooks2 } = await Promise.resolve().then(() => (init_install_hooks(), install_hooks_exports));
|
|
14145
|
+
await installHooks2({ fromInit: true });
|
|
14146
|
+
log.info("Chokepoints armed. Run `cdd-kit doctor` to see live/dormant status; use `--no-arm` next time to skip.");
|
|
14147
|
+
}
|
|
13089
14148
|
log.blank();
|
|
13090
14149
|
}
|
|
13091
14150
|
} catch (err) {
|
|
@@ -13109,9 +14168,9 @@ init_update();
|
|
|
13109
14168
|
|
|
13110
14169
|
// src/commands/new-change.ts
|
|
13111
14170
|
init_paths();
|
|
13112
|
-
import { join as
|
|
14171
|
+
import { join as join12, relative as relative3 } from "path";
|
|
13113
14172
|
import { createHash as createHash4 } from "crypto";
|
|
13114
|
-
import { existsSync as
|
|
14173
|
+
import { existsSync as existsSync11, readFileSync as readFileSync11, readdirSync as readdirSync5, writeFileSync as writeFileSync7 } from "fs";
|
|
13115
14174
|
init_logger();
|
|
13116
14175
|
init_context_scan();
|
|
13117
14176
|
init_digest();
|
|
@@ -13123,10 +14182,10 @@ function inputsDigest2(paths, cwd) {
|
|
|
13123
14182
|
return createHash4("sha256").update(combined).digest("hex");
|
|
13124
14183
|
}
|
|
13125
14184
|
function findContractFiles2(dir, found = []) {
|
|
13126
|
-
if (!
|
|
14185
|
+
if (!existsSync11(dir))
|
|
13127
14186
|
return found;
|
|
13128
14187
|
for (const entry of readdirSync5(dir, { withFileTypes: true })) {
|
|
13129
|
-
const fullPath =
|
|
14188
|
+
const fullPath = join12(dir, entry.name);
|
|
13130
14189
|
if (entry.isDirectory())
|
|
13131
14190
|
findContractFiles2(fullPath, found);
|
|
13132
14191
|
else if (entry.isFile() && entry.name.endsWith(".md") && entry.name !== "INDEX.md" && entry.name !== "CHANGELOG.md") {
|
|
@@ -13136,22 +14195,22 @@ function findContractFiles2(dir, found = []) {
|
|
|
13136
14195
|
return found;
|
|
13137
14196
|
}
|
|
13138
14197
|
function readIndexDigest(filePath) {
|
|
13139
|
-
if (!
|
|
14198
|
+
if (!existsSync11(filePath))
|
|
13140
14199
|
return null;
|
|
13141
|
-
const m =
|
|
14200
|
+
const m = readFileSync11(filePath, "utf8").match(/^inputs-digest:\s*([a-f0-9]+)/m);
|
|
13142
14201
|
return m ? m[1] : null;
|
|
13143
14202
|
}
|
|
13144
14203
|
async function ensureFreshContextIndexes(cwd) {
|
|
13145
|
-
const projectMap =
|
|
13146
|
-
const contractsIndex =
|
|
13147
|
-
const policyPath =
|
|
13148
|
-
const policyInputs = [policyPath].filter(
|
|
13149
|
-
const contractFiles = findContractFiles2(
|
|
14204
|
+
const projectMap = join12(cwd, "specs", "context", "project-map.md");
|
|
14205
|
+
const contractsIndex = join12(cwd, "specs", "context", "contracts-index.md");
|
|
14206
|
+
const policyPath = join12(cwd, ".cdd", "context-policy.json");
|
|
14207
|
+
const policyInputs = [policyPath].filter(existsSync11);
|
|
14208
|
+
const contractFiles = findContractFiles2(join12(cwd, "contracts"));
|
|
13150
14209
|
const wantProjectDigest = inputsDigest2(policyInputs, cwd);
|
|
13151
14210
|
const wantContractsDigest = inputsDigest2(contractFiles, cwd);
|
|
13152
14211
|
const haveProjectDigest = readIndexDigest(projectMap);
|
|
13153
14212
|
const haveContractsDigest = readIndexDigest(contractsIndex);
|
|
13154
|
-
const needsScan = !
|
|
14213
|
+
const needsScan = !existsSync11(projectMap) || !existsSync11(contractsIndex) || haveProjectDigest !== wantProjectDigest || haveContractsDigest !== wantContractsDigest;
|
|
13155
14214
|
if (!needsScan)
|
|
13156
14215
|
return;
|
|
13157
14216
|
log.info("context indexes missing or stale \u2014 running cdd-kit context-scan\u2026");
|
|
@@ -13182,9 +14241,9 @@ function parseDependsOn(raw) {
|
|
|
13182
14241
|
return raw.split(",").map((item) => item.trim()).filter(Boolean);
|
|
13183
14242
|
}
|
|
13184
14243
|
function applyScaffoldMetadata(tasksPath, changeId, dependencies) {
|
|
13185
|
-
if (!
|
|
14244
|
+
if (!existsSync11(tasksPath))
|
|
13186
14245
|
return;
|
|
13187
|
-
let raw =
|
|
14246
|
+
let raw = readFileSync11(tasksPath, "utf8");
|
|
13188
14247
|
raw = raw.replace(/^change-id:\s*<change-id>\s*$/m, `change-id: ${changeId}`);
|
|
13189
14248
|
if (dependencies.length > 0) {
|
|
13190
14249
|
const dependsOn = [
|
|
@@ -13193,7 +14252,7 @@ function applyScaffoldMetadata(tasksPath, changeId, dependencies) {
|
|
|
13193
14252
|
].join("\n");
|
|
13194
14253
|
raw = raw.replace(/^depends-on:\s*\[\]\s*$/m, dependsOn);
|
|
13195
14254
|
}
|
|
13196
|
-
|
|
14255
|
+
writeFileSync7(tasksPath, raw, "utf8");
|
|
13197
14256
|
}
|
|
13198
14257
|
async function newChange(name, opts) {
|
|
13199
14258
|
if (!SAFE_NAME.test(name)) {
|
|
@@ -13208,8 +14267,8 @@ async function newChange(name, opts) {
|
|
|
13208
14267
|
}
|
|
13209
14268
|
}
|
|
13210
14269
|
const cwd = process.cwd();
|
|
13211
|
-
const changeDir =
|
|
13212
|
-
if (
|
|
14270
|
+
const changeDir = join12(cwd, "specs", "changes", name);
|
|
14271
|
+
if (existsSync11(changeDir)) {
|
|
13213
14272
|
if (opts.force) {
|
|
13214
14273
|
log.warn(`Forcing re-scaffold of existing change directory: ${changeDir}`);
|
|
13215
14274
|
log.warn("Existing files will NOT be deleted; only template files will be overwritten.");
|
|
@@ -13232,9 +14291,9 @@ async function newChange(name, opts) {
|
|
|
13232
14291
|
const templates = opts.all ? [...REQUIRED_TEMPLATES, ...listOptional()] : [...REQUIRED_TEMPLATES];
|
|
13233
14292
|
let written = 0;
|
|
13234
14293
|
for (const tmpl of templates) {
|
|
13235
|
-
const src =
|
|
13236
|
-
const dest =
|
|
13237
|
-
if (!
|
|
14294
|
+
const src = join12(ASSET.specsTemplates, tmpl);
|
|
14295
|
+
const dest = join12(changeDir, tmpl);
|
|
14296
|
+
if (!existsSync11(src)) {
|
|
13238
14297
|
log.warn(`Template not found, skipping: ${tmpl}`);
|
|
13239
14298
|
continue;
|
|
13240
14299
|
}
|
|
@@ -13242,7 +14301,7 @@ async function newChange(name, opts) {
|
|
|
13242
14301
|
log.dim(tmpl);
|
|
13243
14302
|
written += 1;
|
|
13244
14303
|
}
|
|
13245
|
-
const tasksPath =
|
|
14304
|
+
const tasksPath = join12(changeDir, "tasks.yml");
|
|
13246
14305
|
applyScaffoldMetadata(tasksPath, name, dependencies);
|
|
13247
14306
|
if (dependencies.length > 0) {
|
|
13248
14307
|
log.dim(`depends-on: ${dependencies.join(", ")}`);
|
|
@@ -13255,9 +14314,9 @@ async function newChange(name, opts) {
|
|
|
13255
14314
|
// src/commands/validate.ts
|
|
13256
14315
|
init_paths();
|
|
13257
14316
|
init_logger();
|
|
13258
|
-
import { join as
|
|
13259
|
-
import { existsSync as
|
|
13260
|
-
import { spawnSync } from "child_process";
|
|
14317
|
+
import { join as join13 } from "path";
|
|
14318
|
+
import { existsSync as existsSync12 } from "fs";
|
|
14319
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
13261
14320
|
var VALIDATORS = [
|
|
13262
14321
|
{
|
|
13263
14322
|
flag: "contracts",
|
|
@@ -13265,6 +14324,7 @@ var VALIDATORS = [
|
|
|
13265
14324
|
label: "contracts",
|
|
13266
14325
|
chain: [
|
|
13267
14326
|
{ script: "validate_api_semantic.py", label: "API semantic" },
|
|
14327
|
+
{ script: "validate_api_conformance.py", label: "API conformance" },
|
|
13268
14328
|
{ script: "validate_env_semantic.py", label: "Env semantic" }
|
|
13269
14329
|
]
|
|
13270
14330
|
},
|
|
@@ -13275,7 +14335,7 @@ var VALIDATORS = [
|
|
|
13275
14335
|
];
|
|
13276
14336
|
function resolvePython() {
|
|
13277
14337
|
for (const cmd of ["python3", "python"]) {
|
|
13278
|
-
const r =
|
|
14338
|
+
const r = spawnSync2(cmd, ["--version"], { stdio: "ignore" });
|
|
13279
14339
|
if (r.status === 0)
|
|
13280
14340
|
return cmd;
|
|
13281
14341
|
}
|
|
@@ -13289,21 +14349,22 @@ async function validate(opts) {
|
|
|
13289
14349
|
log.error(e instanceof Error ? e.message : String(e));
|
|
13290
14350
|
process.exit(1);
|
|
13291
14351
|
}
|
|
13292
|
-
const scriptsDir =
|
|
14352
|
+
const scriptsDir = join13(ASSET.skill, "scripts");
|
|
13293
14353
|
const runAll = !opts.contracts && !opts.env && !opts.ci && !opts.spec && !opts.versions;
|
|
14354
|
+
const pyEnv = { ...process.env, PYTHONUTF8: "1", PYTHONIOENCODING: "utf-8" };
|
|
13294
14355
|
log.blank();
|
|
13295
14356
|
let failed = false;
|
|
13296
14357
|
for (const v of VALIDATORS) {
|
|
13297
14358
|
if (!runAll && !opts[v.flag])
|
|
13298
14359
|
continue;
|
|
13299
|
-
const scriptPath =
|
|
13300
|
-
if (!
|
|
14360
|
+
const scriptPath = join13(scriptsDir, v.script);
|
|
14361
|
+
if (!existsSync12(scriptPath)) {
|
|
13301
14362
|
log.warn(`${v.label}: script not found, skipping (${v.script})`);
|
|
13302
14363
|
log.blank();
|
|
13303
14364
|
continue;
|
|
13304
14365
|
}
|
|
13305
14366
|
log.info(`Validating ${v.label}\u2026`);
|
|
13306
|
-
const r =
|
|
14367
|
+
const r = spawnSync2(py, [scriptPath, ...v.args ?? []], { stdio: "inherit", cwd: process.cwd(), env: pyEnv });
|
|
13307
14368
|
if (r.status !== 0) {
|
|
13308
14369
|
log.error(`${v.label} validation failed.`);
|
|
13309
14370
|
failed = true;
|
|
@@ -13313,14 +14374,14 @@ async function validate(opts) {
|
|
|
13313
14374
|
log.blank();
|
|
13314
14375
|
if (v.chain) {
|
|
13315
14376
|
for (const chained of v.chain) {
|
|
13316
|
-
const chainedPath =
|
|
13317
|
-
if (!
|
|
14377
|
+
const chainedPath = join13(scriptsDir, chained.script);
|
|
14378
|
+
if (!existsSync12(chainedPath)) {
|
|
13318
14379
|
log.warn(`${chained.label}: script not found, skipping (${chained.script})`);
|
|
13319
14380
|
log.blank();
|
|
13320
14381
|
continue;
|
|
13321
14382
|
}
|
|
13322
14383
|
log.info(`Validating ${chained.label}\u2026`);
|
|
13323
|
-
const cr =
|
|
14384
|
+
const cr = spawnSync2(py, [chainedPath], { stdio: "inherit", cwd: process.cwd(), env: pyEnv });
|
|
13324
14385
|
if (cr.status !== 0) {
|
|
13325
14386
|
log.error(`${chained.label} validation failed.`);
|
|
13326
14387
|
failed = true;
|
|
@@ -13344,8 +14405,8 @@ async function validate(opts) {
|
|
|
13344
14405
|
var import_ajv = __toESM(require_ajv(), 1);
|
|
13345
14406
|
var import_ajv_formats = __toESM(require_dist(), 1);
|
|
13346
14407
|
init_logger();
|
|
13347
|
-
import { existsSync as
|
|
13348
|
-
import { join as
|
|
14408
|
+
import { existsSync as existsSync14, readFileSync as readFileSync13, readdirSync as readdirSync6 } from "fs";
|
|
14409
|
+
import { join as join15 } from "path";
|
|
13349
14410
|
import yaml from "js-yaml";
|
|
13350
14411
|
|
|
13351
14412
|
// src/schemas/tasks.schema.ts
|
|
@@ -13374,6 +14435,7 @@ var tasksSchema = {
|
|
|
13374
14435
|
items: { type: "string", pattern: "^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$" },
|
|
13375
14436
|
default: []
|
|
13376
14437
|
},
|
|
14438
|
+
"tier-floor-override": { type: "string", minLength: 1 },
|
|
13377
14439
|
"token-budget": { type: ["string", "number"] },
|
|
13378
14440
|
created: { type: "string" },
|
|
13379
14441
|
completed: { type: "string" },
|
|
@@ -13396,6 +14458,8 @@ var tasksSchema = {
|
|
|
13396
14458
|
};
|
|
13397
14459
|
|
|
13398
14460
|
// src/commands/gate.ts
|
|
14461
|
+
init_tier_floor();
|
|
14462
|
+
init_git_paths();
|
|
13399
14463
|
var ajv = new import_ajv.default({ allErrors: true, allowUnionTypes: true });
|
|
13400
14464
|
(0, import_ajv_formats.default)(ajv);
|
|
13401
14465
|
var validateTasks = ajv.compile(tasksSchema);
|
|
@@ -13433,6 +14497,15 @@ function meaningfulChars(text) {
|
|
|
13433
14497
|
function stripHtmlComments(text) {
|
|
13434
14498
|
return text.replace(/<!--[\s\S]*?-->/g, "");
|
|
13435
14499
|
}
|
|
14500
|
+
var PLACEHOLDER_LITERALS = ["<id>", "<date>", "<change-id>"];
|
|
14501
|
+
var RE_META = /[.*+?^${}()|[\]\\]/g;
|
|
14502
|
+
function findPlaceholders(text) {
|
|
14503
|
+
const clean = stripHtmlComments(text);
|
|
14504
|
+
return PLACEHOLDER_LITERALS.filter((token) => {
|
|
14505
|
+
const re = new RegExp(`:[ \\t]*${token.replace(RE_META, "\\$&")}[ \\t]*\\r?$`, "m");
|
|
14506
|
+
return re.test(clean);
|
|
14507
|
+
}).sort();
|
|
14508
|
+
}
|
|
13436
14509
|
function countPendingExpansions(sectionBody2) {
|
|
13437
14510
|
if (!sectionBody2.trim())
|
|
13438
14511
|
return 0;
|
|
@@ -13454,7 +14527,7 @@ function countPendingContextRequests(content) {
|
|
|
13454
14527
|
}
|
|
13455
14528
|
function loadYamlFile(path) {
|
|
13456
14529
|
try {
|
|
13457
|
-
const raw =
|
|
14530
|
+
const raw = readFileSync13(path, "utf8");
|
|
13458
14531
|
return { data: yaml.load(raw, { schema: yaml.JSON_SCHEMA }), parseError: null };
|
|
13459
14532
|
} catch (err) {
|
|
13460
14533
|
return { data: null, parseError: err.message };
|
|
@@ -13485,8 +14558,8 @@ function ajvErrorsToMessages(errors, prefix, knownKeys) {
|
|
|
13485
14558
|
return out;
|
|
13486
14559
|
}
|
|
13487
14560
|
function isContextGovernedChange(changeDir) {
|
|
13488
|
-
const tasksPath =
|
|
13489
|
-
if (!
|
|
14561
|
+
const tasksPath = join15(changeDir, "tasks.yml");
|
|
14562
|
+
if (!existsSync14(tasksPath))
|
|
13490
14563
|
return false;
|
|
13491
14564
|
const { data } = loadYamlFile(tasksPath);
|
|
13492
14565
|
return data?.["context-governance"] === "v1";
|
|
@@ -13514,12 +14587,12 @@ function lintTasksFile(tasksPath, errors, warnings) {
|
|
|
13514
14587
|
return data;
|
|
13515
14588
|
}
|
|
13516
14589
|
function resolveTier(changeDir) {
|
|
13517
|
-
const classifPath =
|
|
13518
|
-
const classificationPresent =
|
|
13519
|
-
const classificationText = classificationPresent ?
|
|
14590
|
+
const classifPath = join15(changeDir, "change-classification.md");
|
|
14591
|
+
const classificationPresent = existsSync14(classifPath);
|
|
14592
|
+
const classificationText = classificationPresent ? readFileSync13(classifPath, "utf8") : "";
|
|
13520
14593
|
const classificationHasLooseMarker = classificationPresent && TIER_PATTERN.test(classificationText);
|
|
13521
|
-
const tasksPath =
|
|
13522
|
-
if (
|
|
14594
|
+
const tasksPath = join15(changeDir, "tasks.yml");
|
|
14595
|
+
if (existsSync14(tasksPath)) {
|
|
13523
14596
|
const { data } = loadYamlFile(tasksPath);
|
|
13524
14597
|
const t = data?.tier;
|
|
13525
14598
|
if (typeof t === "number" && t >= 0 && t <= 5) {
|
|
@@ -13565,7 +14638,7 @@ function enforceTierConsistency(changeDir, errors, warnings) {
|
|
|
13565
14638
|
return;
|
|
13566
14639
|
}
|
|
13567
14640
|
if (resolution.source === "tasks-frontmatter" && resolution.classificationPresent) {
|
|
13568
|
-
const text =
|
|
14641
|
+
const text = readFileSync13(join15(changeDir, "change-classification.md"), "utf8");
|
|
13569
14642
|
const structured = text.match(/^## Tier\s*\n\s*-\s*(\d)\s*$/m);
|
|
13570
14643
|
const bold = text.match(/\*\*Tier:\*\*\s*Tier\s*(\d)\b/i);
|
|
13571
14644
|
const classifTier = structured ? parseInt(structured[1], 10) : bold ? parseInt(bold[1], 10) : NaN;
|
|
@@ -13576,12 +14649,50 @@ function enforceTierConsistency(changeDir, errors, warnings) {
|
|
|
13576
14649
|
}
|
|
13577
14650
|
}
|
|
13578
14651
|
}
|
|
14652
|
+
function enforceTierFloor(changeDir, errors, warnings) {
|
|
14653
|
+
const cwd = process.cwd();
|
|
14654
|
+
const policy = loadTierPolicy(cwd);
|
|
14655
|
+
if (!policy.enabled)
|
|
14656
|
+
return;
|
|
14657
|
+
const requestPath = join15(changeDir, "change-request.md");
|
|
14658
|
+
const requestText = existsSync14(requestPath) ? readFileSync13(requestPath, "utf8") : "";
|
|
14659
|
+
const staged = getStagedPaths(cwd);
|
|
14660
|
+
const stagedChangeIds = new Set(
|
|
14661
|
+
staged.map((p) => /^specs\/changes\/([^/]+)\//.exec(p)?.[1]).filter((id) => id !== void 0)
|
|
14662
|
+
);
|
|
14663
|
+
const touched = stagedChangeIds.size > 1 ? [] : staged;
|
|
14664
|
+
const floor = computeTierFloor(requestText, policy, { paths: touched });
|
|
14665
|
+
if (floor.floorTier === null)
|
|
14666
|
+
return;
|
|
14667
|
+
const declared = resolveTier(changeDir).tier;
|
|
14668
|
+
if (declared === null) {
|
|
14669
|
+
warnings.push(
|
|
14670
|
+
`tier floor: ${floor.label} detected (matched: ${floor.matched.join(", ")}); classification must declare tier ${floor.floorTier} or stricter.`
|
|
14671
|
+
);
|
|
14672
|
+
return;
|
|
14673
|
+
}
|
|
14674
|
+
if (declared <= floor.floorTier)
|
|
14675
|
+
return;
|
|
14676
|
+
const overrideRaw = (() => {
|
|
14677
|
+
const { data } = loadYamlFile(join15(changeDir, "tasks.yml"));
|
|
14678
|
+
const o = data?.["tier-floor-override"];
|
|
14679
|
+
return typeof o === "string" ? o.trim() : "";
|
|
14680
|
+
})();
|
|
14681
|
+
const detail = `${floor.label} detected (matched: ${floor.matched.join(", ")}) requires tier ${floor.floorTier} or stricter, but classification declared tier ${declared}.`;
|
|
14682
|
+
if (overrideRaw) {
|
|
14683
|
+
warnings.push(`tier floor override: ${detail} Bypassed by tier-floor-override: "${overrideRaw}".`);
|
|
14684
|
+
} else {
|
|
14685
|
+
errors.push(
|
|
14686
|
+
`tier floor violation: ${detail} Re-classify to tier ${floor.floorTier} (or stricter), or record \`tier-floor-override: "<reason>"\` in tasks.yml frontmatter to bypass with an audit trail. Disable entirely in .cdd/tier-policy.json.`
|
|
14687
|
+
);
|
|
14688
|
+
}
|
|
14689
|
+
}
|
|
13579
14690
|
function isArchivedChange(cwd, changeId) {
|
|
13580
|
-
const archiveRoot =
|
|
13581
|
-
if (!
|
|
14691
|
+
const archiveRoot = join15(cwd, "specs", "archive");
|
|
14692
|
+
if (!existsSync14(archiveRoot))
|
|
13582
14693
|
return false;
|
|
13583
14694
|
const years = readdirSync6(archiveRoot, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
13584
|
-
return years.some((year) =>
|
|
14695
|
+
return years.some((year) => existsSync14(join15(archiveRoot, year.name, changeId)));
|
|
13585
14696
|
}
|
|
13586
14697
|
function detectDependencyCycle(cwd, startChangeId) {
|
|
13587
14698
|
const visited = /* @__PURE__ */ new Set();
|
|
@@ -13594,8 +14705,8 @@ function detectDependencyCycle(cwd, startChangeId) {
|
|
|
13594
14705
|
return null;
|
|
13595
14706
|
visited.add(id);
|
|
13596
14707
|
stack.push(id);
|
|
13597
|
-
const tasksPath =
|
|
13598
|
-
if (
|
|
14708
|
+
const tasksPath = join15(cwd, "specs", "changes", id, "tasks.yml");
|
|
14709
|
+
if (existsSync14(tasksPath)) {
|
|
13599
14710
|
const { data } = loadYamlFile(tasksPath);
|
|
13600
14711
|
const deps = data?.["depends-on"] ?? [];
|
|
13601
14712
|
for (const dep of deps) {
|
|
@@ -13610,8 +14721,8 @@ function detectDependencyCycle(cwd, startChangeId) {
|
|
|
13610
14721
|
return visit(startChangeId);
|
|
13611
14722
|
}
|
|
13612
14723
|
function validateDependencies(cwd, changeId, changeDir) {
|
|
13613
|
-
const tasksPath =
|
|
13614
|
-
if (!
|
|
14724
|
+
const tasksPath = join15(changeDir, "tasks.yml");
|
|
14725
|
+
if (!existsSync14(tasksPath))
|
|
13615
14726
|
return [];
|
|
13616
14727
|
const { data } = loadYamlFile(tasksPath);
|
|
13617
14728
|
const dependencies = data?.["depends-on"] ?? [];
|
|
@@ -13625,10 +14736,10 @@ function validateDependencies(cwd, changeId, changeDir) {
|
|
|
13625
14736
|
errors.push(`tasks.yml: change cannot depend on itself (${dep})`);
|
|
13626
14737
|
continue;
|
|
13627
14738
|
}
|
|
13628
|
-
const upstreamDir =
|
|
13629
|
-
if (
|
|
13630
|
-
const upstreamTasks =
|
|
13631
|
-
if (!
|
|
14739
|
+
const upstreamDir = join15(cwd, "specs", "changes", dep);
|
|
14740
|
+
if (existsSync14(upstreamDir)) {
|
|
14741
|
+
const upstreamTasks = join15(upstreamDir, "tasks.yml");
|
|
14742
|
+
if (!existsSync14(upstreamTasks)) {
|
|
13632
14743
|
errors.push(`dependency ${dep}: missing tasks.yml`);
|
|
13633
14744
|
continue;
|
|
13634
14745
|
}
|
|
@@ -13648,19 +14759,19 @@ function validateDependencies(cwd, changeId, changeDir) {
|
|
|
13648
14759
|
async function gate(changeId, opts = {}) {
|
|
13649
14760
|
const strict = opts.strict ?? false;
|
|
13650
14761
|
const cwd = process.cwd();
|
|
13651
|
-
const changeDir =
|
|
13652
|
-
if (!
|
|
14762
|
+
const changeDir = join15(cwd, "specs", "changes", changeId);
|
|
14763
|
+
if (!existsSync14(changeDir)) {
|
|
13653
14764
|
log.error(`change not found: ${changeId} (looked in ${changeDir})`);
|
|
13654
14765
|
process.exit(1);
|
|
13655
14766
|
}
|
|
13656
14767
|
const errors = [];
|
|
13657
14768
|
const warnings = [];
|
|
13658
14769
|
const isNewChange = isContextGovernedChange(changeDir);
|
|
13659
|
-
const manifestPath =
|
|
13660
|
-
const hasManifest =
|
|
14770
|
+
const manifestPath = join15(changeDir, "context-manifest.md");
|
|
14771
|
+
const hasManifest = existsSync14(manifestPath);
|
|
13661
14772
|
errors.push(...validateDependencies(cwd, changeId, changeDir));
|
|
13662
14773
|
if (hasManifest) {
|
|
13663
|
-
const pending = countPendingContextRequests(
|
|
14774
|
+
const pending = countPendingContextRequests(readFileSync13(manifestPath, "utf8"));
|
|
13664
14775
|
if (pending > 0) {
|
|
13665
14776
|
warnings.push(`context-manifest.md: has ${pending} pending context expansion request(s)`);
|
|
13666
14777
|
}
|
|
@@ -13676,7 +14787,7 @@ async function gate(changeId, opts = {}) {
|
|
|
13676
14787
|
}
|
|
13677
14788
|
continue;
|
|
13678
14789
|
}
|
|
13679
|
-
if (!
|
|
14790
|
+
if (!existsSync14(join15(changeDir, f))) {
|
|
13680
14791
|
errors.push(`missing required artifact: ${f}`);
|
|
13681
14792
|
}
|
|
13682
14793
|
}
|
|
@@ -13686,21 +14797,30 @@ async function gate(changeId, opts = {}) {
|
|
|
13686
14797
|
continue;
|
|
13687
14798
|
if (f === "tasks.yml")
|
|
13688
14799
|
continue;
|
|
13689
|
-
const content =
|
|
14800
|
+
const content = readFileSync13(join15(changeDir, f), "utf8");
|
|
13690
14801
|
const minChars = MIN_CHARS[f] ?? 100;
|
|
13691
14802
|
if (meaningfulChars(content) < minChars) {
|
|
13692
14803
|
errors.push(`${f}: appears to be a stub (< ${minChars} meaningful chars)`);
|
|
14804
|
+
continue;
|
|
14805
|
+
}
|
|
14806
|
+
if (f !== "context-manifest.md") {
|
|
14807
|
+
const placeholders = findPlaceholders(content);
|
|
14808
|
+
if (placeholders.length > 0) {
|
|
14809
|
+
errors.push(
|
|
14810
|
+
`${f}: still contains unfilled template placeholder(s) ${placeholders.join(", ")} \u2014 replace them with the change's real values before the gate can pass`
|
|
14811
|
+
);
|
|
14812
|
+
}
|
|
13693
14813
|
}
|
|
13694
14814
|
}
|
|
13695
|
-
const classifPath =
|
|
14815
|
+
const classifPath = join15(changeDir, "change-classification.md");
|
|
13696
14816
|
const tierResolution = resolveTier(changeDir);
|
|
13697
|
-
if (tierResolution.tier === null &&
|
|
14817
|
+
if (tierResolution.tier === null && existsSync14(classifPath) && !tierResolution.classificationHasLooseMarker) {
|
|
13698
14818
|
errors.push("change-classification.md: missing tier/risk marker (set tier in tasks.yml frontmatter, or include Tier 0-5 / low|medium|high|critical in change-classification.md)");
|
|
13699
14819
|
}
|
|
13700
14820
|
}
|
|
13701
|
-
const tasksPath =
|
|
14821
|
+
const tasksPath = join15(changeDir, "tasks.yml");
|
|
13702
14822
|
let tasksData = null;
|
|
13703
|
-
if (
|
|
14823
|
+
if (existsSync14(tasksPath)) {
|
|
13704
14824
|
tasksData = lintTasksFile(tasksPath, errors, warnings);
|
|
13705
14825
|
}
|
|
13706
14826
|
if (tasksData) {
|
|
@@ -13715,6 +14835,7 @@ async function gate(changeId, opts = {}) {
|
|
|
13715
14835
|
}
|
|
13716
14836
|
}
|
|
13717
14837
|
enforceTierConsistency(changeDir, errors, warnings);
|
|
14838
|
+
enforceTierFloor(changeDir, errors, warnings);
|
|
13718
14839
|
for (const w of warnings) {
|
|
13719
14840
|
log.warn(` ${w}`);
|
|
13720
14841
|
}
|
|
@@ -13738,75 +14859,540 @@ async function gate(changeId, opts = {}) {
|
|
|
13738
14859
|
log.ok(`gate passed for change: ${changeId}`);
|
|
13739
14860
|
}
|
|
13740
14861
|
|
|
13741
|
-
// src/
|
|
13742
|
-
|
|
14862
|
+
// src/cli/index.ts
|
|
14863
|
+
init_install_hooks();
|
|
14864
|
+
init_install_agent_hooks();
|
|
14865
|
+
|
|
14866
|
+
// src/commands/openapi-export.ts
|
|
13743
14867
|
init_logger();
|
|
13744
|
-
import { existsSync as
|
|
13745
|
-
import {
|
|
13746
|
-
var
|
|
13747
|
-
var
|
|
13748
|
-
|
|
13749
|
-
const
|
|
13750
|
-
|
|
13751
|
-
|
|
13752
|
-
|
|
13753
|
-
|
|
14868
|
+
import { existsSync as existsSync15, readFileSync as readFileSync14, writeFileSync as writeFileSync8, mkdirSync as mkdirSync7 } from "fs";
|
|
14869
|
+
import { dirname as dirname4, resolve as resolve2 } from "path";
|
|
14870
|
+
var DEFAULT_CONTRACT = "contracts/api/api-contract.md";
|
|
14871
|
+
var VALID_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "delete", "patch", "head", "options"]);
|
|
14872
|
+
function stripFrontmatter(text) {
|
|
14873
|
+
const fm = {};
|
|
14874
|
+
if (text.startsWith("---")) {
|
|
14875
|
+
const end = text.indexOf("\n---", 3);
|
|
14876
|
+
if (end !== -1) {
|
|
14877
|
+
const block = text.slice(3, end);
|
|
14878
|
+
for (const line of block.split("\n")) {
|
|
14879
|
+
const m = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
|
14880
|
+
if (m)
|
|
14881
|
+
fm[m[1].trim()] = m[2].trim();
|
|
14882
|
+
}
|
|
14883
|
+
return { body: text.slice(end + 4).replace(/^\n+/, ""), frontmatter: fm };
|
|
14884
|
+
}
|
|
13754
14885
|
}
|
|
13755
|
-
|
|
13756
|
-
|
|
13757
|
-
|
|
13758
|
-
|
|
13759
|
-
|
|
13760
|
-
|
|
13761
|
-
|
|
13762
|
-
|
|
13763
|
-
|
|
13764
|
-
|
|
13765
|
-
|
|
13766
|
-
|
|
13767
|
-
|
|
13768
|
-
|
|
13769
|
-
|
|
13770
|
-
|
|
13771
|
-
|
|
13772
|
-
|
|
13773
|
-
|
|
13774
|
-
|
|
13775
|
-
|
|
13776
|
-
|
|
13777
|
-
|
|
13778
|
-
|
|
13779
|
-
|
|
13780
|
-
|
|
13781
|
-
|
|
14886
|
+
return { body: text, frontmatter: fm };
|
|
14887
|
+
}
|
|
14888
|
+
function parseRow(line) {
|
|
14889
|
+
return line.trim().replace(/^\|/, "").replace(/\|$/, "").split("|").map((c) => c.trim());
|
|
14890
|
+
}
|
|
14891
|
+
function isSeparator(cells) {
|
|
14892
|
+
return cells.every((c) => c === "" || /^:?-+:?$/.test(c));
|
|
14893
|
+
}
|
|
14894
|
+
function parseEndpoints(body) {
|
|
14895
|
+
const rows = [];
|
|
14896
|
+
let inTable = false;
|
|
14897
|
+
let sepSeen = false;
|
|
14898
|
+
for (const raw of body.split("\n")) {
|
|
14899
|
+
const line = raw.trim();
|
|
14900
|
+
if (!line || !line.startsWith("|"))
|
|
14901
|
+
continue;
|
|
14902
|
+
const cells = parseRow(line);
|
|
14903
|
+
if (cells[0]?.toLowerCase() === "method") {
|
|
14904
|
+
inTable = true;
|
|
14905
|
+
sepSeen = false;
|
|
14906
|
+
continue;
|
|
14907
|
+
}
|
|
14908
|
+
if (!inTable)
|
|
14909
|
+
continue;
|
|
14910
|
+
if (!sepSeen && isSeparator(cells)) {
|
|
14911
|
+
sepSeen = true;
|
|
14912
|
+
continue;
|
|
14913
|
+
}
|
|
14914
|
+
if (cells.length < 2 || !cells.some(Boolean))
|
|
14915
|
+
continue;
|
|
14916
|
+
const method = (cells[0] ?? "").toLowerCase();
|
|
14917
|
+
const path = cells[1] ?? "";
|
|
14918
|
+
if (!VALID_METHODS.has(method) || !path.startsWith("/"))
|
|
14919
|
+
continue;
|
|
14920
|
+
rows.push({
|
|
14921
|
+
method,
|
|
14922
|
+
path,
|
|
14923
|
+
auth: (cells[2] ?? "").toLowerCase(),
|
|
14924
|
+
request: cells[3] ?? "",
|
|
14925
|
+
response: cells[4] ?? "",
|
|
14926
|
+
errors: cells[5] ?? ""
|
|
14927
|
+
});
|
|
14928
|
+
}
|
|
14929
|
+
return rows;
|
|
14930
|
+
}
|
|
14931
|
+
var SCHEMA_NAME_RE = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
14932
|
+
var PRIMITIVE_TYPES = /* @__PURE__ */ new Set(["string", "integer", "number", "boolean"]);
|
|
14933
|
+
function extractSchemasSection(body) {
|
|
14934
|
+
const lines = body.split("\n");
|
|
14935
|
+
const start = lines.findIndex((line) => /^##\s+Schemas\s*$/i.test(line.trim()));
|
|
14936
|
+
if (start === -1)
|
|
14937
|
+
return "";
|
|
14938
|
+
const out = [];
|
|
14939
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
14940
|
+
if (/^##\s+/.test(lines[i].trim()))
|
|
14941
|
+
break;
|
|
14942
|
+
out.push(lines[i]);
|
|
14943
|
+
}
|
|
14944
|
+
return out.join("\n");
|
|
14945
|
+
}
|
|
14946
|
+
function parseSchemaSections(body) {
|
|
14947
|
+
const schemasBlock = extractSchemasSection(body).replace(/<!--[\s\S]*?-->/g, "");
|
|
14948
|
+
if (!schemasBlock.trim())
|
|
14949
|
+
return { sections: [], errors: [] };
|
|
14950
|
+
const sections = [];
|
|
14951
|
+
const errors = [];
|
|
14952
|
+
const seen = /* @__PURE__ */ new Set();
|
|
14953
|
+
const headingRe = /^###\s+(.+?)\s*$/gm;
|
|
14954
|
+
const headings = Array.from(schemasBlock.matchAll(headingRe));
|
|
14955
|
+
for (let i = 0; i < headings.length; i += 1) {
|
|
14956
|
+
const heading = headings[i];
|
|
14957
|
+
const name = (heading[1] ?? "").trim();
|
|
14958
|
+
const contentStart = (heading.index ?? 0) + heading[0].length;
|
|
14959
|
+
const contentEnd = i + 1 < headings.length ? headings[i + 1].index ?? schemasBlock.length : schemasBlock.length;
|
|
14960
|
+
if (!SCHEMA_NAME_RE.test(name))
|
|
14961
|
+
continue;
|
|
14962
|
+
if (seen.has(name)) {
|
|
14963
|
+
errors.push(`Duplicate schema section: ${name}`);
|
|
14964
|
+
continue;
|
|
14965
|
+
}
|
|
14966
|
+
seen.add(name);
|
|
14967
|
+
sections.push({ name, content: schemasBlock.slice(contentStart, contentEnd) });
|
|
14968
|
+
}
|
|
14969
|
+
return { sections, errors };
|
|
14970
|
+
}
|
|
14971
|
+
function parseJsonSchemaBlocks(section) {
|
|
14972
|
+
const blocks = [];
|
|
14973
|
+
const errors = [];
|
|
14974
|
+
const blockRe = /```json-schema\s*\n([\s\S]*?)```/g;
|
|
14975
|
+
for (const match of section.content.matchAll(blockRe)) {
|
|
14976
|
+
try {
|
|
14977
|
+
const parsed = JSON.parse(match[1] ?? "");
|
|
14978
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
14979
|
+
errors.push(`Schema ${section.name}: json-schema block must be a JSON object`);
|
|
13782
14980
|
} else {
|
|
13783
|
-
|
|
14981
|
+
blocks.push(parsed);
|
|
13784
14982
|
}
|
|
14983
|
+
} catch (err) {
|
|
14984
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
14985
|
+
errors.push(`Schema ${section.name}: invalid json-schema block (${detail})`);
|
|
13785
14986
|
}
|
|
13786
14987
|
}
|
|
13787
|
-
|
|
13788
|
-
|
|
13789
|
-
|
|
13790
|
-
|
|
14988
|
+
return { blocks, errors };
|
|
14989
|
+
}
|
|
14990
|
+
function findForeignFenceTag(section) {
|
|
14991
|
+
const fenceRe = /```([^\n]*)(?:\n|$)[\s\S]*?```/g;
|
|
14992
|
+
for (const match of section.content.matchAll(fenceRe)) {
|
|
14993
|
+
const info = (match[1] ?? "").trim();
|
|
14994
|
+
if (info !== "json-schema")
|
|
14995
|
+
return info;
|
|
13791
14996
|
}
|
|
13792
|
-
|
|
13793
|
-
|
|
14997
|
+
return null;
|
|
14998
|
+
}
|
|
14999
|
+
function parseFieldTable(section) {
|
|
15000
|
+
const lines = section.content.split("\n");
|
|
15001
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
15002
|
+
const line = lines[i].trim();
|
|
15003
|
+
if (!line.startsWith("|"))
|
|
15004
|
+
continue;
|
|
15005
|
+
const header = parseRow(line).map((c) => c.toLowerCase());
|
|
15006
|
+
const fieldIdx = header.indexOf("field");
|
|
15007
|
+
const typeIdx = header.indexOf("type");
|
|
15008
|
+
const requiredIdx = header.indexOf("required");
|
|
15009
|
+
if (fieldIdx === -1 || typeIdx === -1 || requiredIdx === -1)
|
|
15010
|
+
continue;
|
|
15011
|
+
const separator = lines[i + 1]?.trim();
|
|
15012
|
+
if (!separator?.startsWith("|") || !isSeparator(parseRow(separator)))
|
|
15013
|
+
continue;
|
|
15014
|
+
const notesIdx = header.indexOf("notes");
|
|
15015
|
+
const formatIdx = header.indexOf("format");
|
|
15016
|
+
const rows = [];
|
|
15017
|
+
for (let j = i + 2; j < lines.length; j += 1) {
|
|
15018
|
+
const rowLine = lines[j].trim();
|
|
15019
|
+
if (!rowLine || !rowLine.startsWith("|"))
|
|
15020
|
+
break;
|
|
15021
|
+
const cells = parseRow(rowLine);
|
|
15022
|
+
if (isSeparator(cells) || cells.length < 2 || !cells.some(Boolean))
|
|
15023
|
+
continue;
|
|
15024
|
+
rows.push({
|
|
15025
|
+
field: cells[fieldIdx] ?? "",
|
|
15026
|
+
type: cells[typeIdx] ?? "",
|
|
15027
|
+
required: cells[requiredIdx] ?? "",
|
|
15028
|
+
notes: notesIdx === -1 ? "" : cells[notesIdx] ?? "",
|
|
15029
|
+
format: formatIdx === -1 ? "" : cells[formatIdx] ?? ""
|
|
15030
|
+
});
|
|
15031
|
+
}
|
|
15032
|
+
return { rows, found: true };
|
|
15033
|
+
}
|
|
15034
|
+
return { rows: [], found: false };
|
|
15035
|
+
}
|
|
15036
|
+
function compileType(typeValue, schemaNames, resolvableNames, context2) {
|
|
15037
|
+
const type = typeValue.trim();
|
|
15038
|
+
if (!type)
|
|
15039
|
+
throw new Error(`${context2}: empty type`);
|
|
15040
|
+
if (type.endsWith("[]")) {
|
|
15041
|
+
const inner = type.slice(0, -2).trim();
|
|
15042
|
+
if (!inner || inner.endsWith("[]"))
|
|
15043
|
+
throw new Error(`${context2}: unsupported array type "${type}"`);
|
|
15044
|
+
return { type: "array", items: compileType(inner, schemaNames, resolvableNames, context2) };
|
|
15045
|
+
}
|
|
15046
|
+
if (PRIMITIVE_TYPES.has(type))
|
|
15047
|
+
return { type };
|
|
15048
|
+
const enumMatch = type.match(/^enum\((.*)\)$/);
|
|
15049
|
+
if (enumMatch) {
|
|
15050
|
+
const values = (enumMatch[1] ?? "").split(",").map((v) => v.trim()).filter(Boolean);
|
|
15051
|
+
if (values.length === 0)
|
|
15052
|
+
throw new Error(`${context2}: enum must list at least one value`);
|
|
15053
|
+
return { type: "string", enum: values };
|
|
15054
|
+
}
|
|
15055
|
+
if (SCHEMA_NAME_RE.test(type) && schemaNames.has(type)) {
|
|
15056
|
+
if (!resolvableNames.has(type)) {
|
|
15057
|
+
throw new Error(`${context2}: referenced schema "${type}" has no field table or json-schema block`);
|
|
15058
|
+
}
|
|
15059
|
+
return { $ref: `#/components/schemas/${type}` };
|
|
15060
|
+
}
|
|
15061
|
+
throw new Error(`${context2}: unknown type "${type}"`);
|
|
15062
|
+
}
|
|
15063
|
+
function compileFieldTable(section, rows, schemaNames, resolvableNames) {
|
|
15064
|
+
const properties = {};
|
|
15065
|
+
const required = [];
|
|
15066
|
+
for (const row of rows) {
|
|
15067
|
+
const field = row.field.trim();
|
|
15068
|
+
if (!field)
|
|
15069
|
+
throw new Error(`Schema ${section.name}: field name is required`);
|
|
15070
|
+
const schema = compileType(row.type, schemaNames, resolvableNames, `Schema ${section.name}, field ${field}`);
|
|
15071
|
+
if (row.notes.trim())
|
|
15072
|
+
schema.description = row.notes.trim();
|
|
15073
|
+
if (row.format.trim())
|
|
15074
|
+
schema.format = row.format.trim();
|
|
15075
|
+
properties[field] = schema;
|
|
15076
|
+
if (row.required.trim().toLowerCase() === "yes")
|
|
15077
|
+
required.push(field);
|
|
15078
|
+
}
|
|
15079
|
+
const compiled = { type: "object", properties };
|
|
15080
|
+
if (required.length > 0)
|
|
15081
|
+
compiled.required = required;
|
|
15082
|
+
return compiled;
|
|
15083
|
+
}
|
|
15084
|
+
function parseContractSchemas(body) {
|
|
15085
|
+
const { sections, errors } = parseSchemaSections(body);
|
|
15086
|
+
if (sections.length === 0)
|
|
15087
|
+
return { schemas: {}, errors };
|
|
15088
|
+
const schemaNames = new Set(sections.map((s) => s.name));
|
|
15089
|
+
const metadata = /* @__PURE__ */ new Map();
|
|
15090
|
+
const resolvableNames = /* @__PURE__ */ new Set();
|
|
15091
|
+
for (const section of sections) {
|
|
15092
|
+
const raw = parseJsonSchemaBlocks(section);
|
|
15093
|
+
errors.push(...raw.errors);
|
|
15094
|
+
if (raw.blocks.length > 1)
|
|
15095
|
+
errors.push(`Schema ${section.name}: expected at most one json-schema block`);
|
|
15096
|
+
const fields = parseFieldTable(section);
|
|
15097
|
+
if (raw.blocks.length > 0 && fields.found) {
|
|
15098
|
+
errors.push(`Schema ${section.name}: choose either a field table or a json-schema block, not both`);
|
|
15099
|
+
}
|
|
15100
|
+
const foreign = findForeignFenceTag(section);
|
|
15101
|
+
if (foreign !== null) {
|
|
15102
|
+
const desc = foreign ? `a \`\`\`${foreign} code block` : "an untagged code block";
|
|
15103
|
+
errors.push(
|
|
15104
|
+
`Schema ${section.name}: found ${desc}, but a machine-typed schema body must use a \`\`\`json-schema fence. Change the fence to \`\`\`json-schema, use a "| field | type | required | ... |" table, or remove the fence to keep it a free-form (Tier C) prose contract.`
|
|
15105
|
+
);
|
|
15106
|
+
}
|
|
15107
|
+
if (raw.blocks.length === 1 || fields.found)
|
|
15108
|
+
resolvableNames.add(section.name);
|
|
15109
|
+
metadata.set(section.name, {
|
|
15110
|
+
section,
|
|
15111
|
+
rawBlocks: raw.blocks,
|
|
15112
|
+
fieldRows: fields.rows,
|
|
15113
|
+
hasFieldTable: fields.found
|
|
15114
|
+
});
|
|
15115
|
+
}
|
|
15116
|
+
const schemas = {};
|
|
15117
|
+
for (const [name, item] of metadata) {
|
|
15118
|
+
if (item.rawBlocks.length === 1 && !item.hasFieldTable) {
|
|
15119
|
+
schemas[name] = item.rawBlocks[0];
|
|
15120
|
+
continue;
|
|
15121
|
+
}
|
|
15122
|
+
if (!item.hasFieldTable)
|
|
15123
|
+
continue;
|
|
15124
|
+
try {
|
|
15125
|
+
schemas[name] = compileFieldTable(item.section, item.fieldRows, schemaNames, resolvableNames);
|
|
15126
|
+
} catch (err) {
|
|
15127
|
+
errors.push(err instanceof Error ? err.message : String(err));
|
|
15128
|
+
}
|
|
15129
|
+
}
|
|
15130
|
+
return { schemas, errors };
|
|
15131
|
+
}
|
|
15132
|
+
function resolveSchemaCell(cell, schemas) {
|
|
15133
|
+
const raw = cell.trim();
|
|
15134
|
+
if (!raw || raw === "-")
|
|
15135
|
+
return void 0;
|
|
15136
|
+
const isArray = raw.endsWith("[]");
|
|
15137
|
+
const name = isArray ? raw.slice(0, -2).trim() : raw;
|
|
15138
|
+
if (!SCHEMA_NAME_RE.test(name) || !schemas[name])
|
|
15139
|
+
return void 0;
|
|
15140
|
+
const ref = { $ref: `#/components/schemas/${name}` };
|
|
15141
|
+
return isArray ? { type: "array", items: ref } : ref;
|
|
15142
|
+
}
|
|
15143
|
+
function toOpenApiPath(path) {
|
|
15144
|
+
const params = [];
|
|
15145
|
+
const seen = /* @__PURE__ */ new Set();
|
|
15146
|
+
const addParam = (name) => {
|
|
15147
|
+
if (name && !seen.has(name)) {
|
|
15148
|
+
seen.add(name);
|
|
15149
|
+
params.push({ name, in: "path", required: true, schema: { type: "string" } });
|
|
15150
|
+
}
|
|
15151
|
+
};
|
|
15152
|
+
let oapi = path.replace(/:([A-Za-z_][\w]*)/g, (_m, n) => {
|
|
15153
|
+
addParam(n);
|
|
15154
|
+
return `{${n}}`;
|
|
15155
|
+
}).replace(/\{([^}/]+)\}/g, (_m, n) => {
|
|
15156
|
+
addParam(n.trim());
|
|
15157
|
+
return `{${n.trim()}}`;
|
|
15158
|
+
});
|
|
15159
|
+
oapi = oapi.split("?", 1)[0];
|
|
15160
|
+
return { path: oapi, params };
|
|
15161
|
+
}
|
|
15162
|
+
function authToSecurity(auth) {
|
|
15163
|
+
switch (auth) {
|
|
15164
|
+
case "required":
|
|
15165
|
+
case "admin":
|
|
15166
|
+
return { security: [{ bearerAuth: [] }], scheme: "bearerAuth" };
|
|
15167
|
+
case "optional":
|
|
15168
|
+
return { security: [{ bearerAuth: [] }, {}], scheme: "bearerAuth" };
|
|
15169
|
+
case "none":
|
|
15170
|
+
case "public":
|
|
15171
|
+
case "":
|
|
15172
|
+
return {};
|
|
15173
|
+
default:
|
|
15174
|
+
return { security: [{ bearerAuth: [] }], scheme: "bearerAuth" };
|
|
15175
|
+
}
|
|
15176
|
+
}
|
|
15177
|
+
function statusFromErrors(errors) {
|
|
15178
|
+
const codes = (errors.match(/\b[1-5]\d\d\b/g) ?? []).filter((v, i, a) => a.indexOf(v) === i);
|
|
15179
|
+
return codes;
|
|
15180
|
+
}
|
|
15181
|
+
function buildDoc(endpoints, frontmatter, styleBlock, schemas) {
|
|
15182
|
+
const paths = {};
|
|
15183
|
+
let anySecurity = false;
|
|
15184
|
+
for (const ep of endpoints) {
|
|
15185
|
+
const { path, params } = toOpenApiPath(ep.path);
|
|
15186
|
+
const successCode = ep.method === "post" ? "201" : "200";
|
|
15187
|
+
const responseSchema = resolveSchemaCell(ep.response, schemas);
|
|
15188
|
+
const responses = {
|
|
15189
|
+
[successCode]: { description: ep.response ? `Contract response: ${ep.response}` : "Success" }
|
|
15190
|
+
};
|
|
15191
|
+
if (responseSchema) {
|
|
15192
|
+
responses[successCode].content = {
|
|
15193
|
+
"application/json": { schema: responseSchema }
|
|
15194
|
+
};
|
|
15195
|
+
}
|
|
15196
|
+
for (const code of statusFromErrors(ep.errors)) {
|
|
15197
|
+
if (!responses[code])
|
|
15198
|
+
responses[code] = { description: "Error response (see contract error format)" };
|
|
15199
|
+
}
|
|
15200
|
+
const op = {
|
|
15201
|
+
summary: `${ep.method.toUpperCase()} ${ep.path}`,
|
|
15202
|
+
responses
|
|
15203
|
+
};
|
|
15204
|
+
if (params.length > 0)
|
|
15205
|
+
op.parameters = params;
|
|
15206
|
+
const { security, scheme } = authToSecurity(ep.auth);
|
|
15207
|
+
if (security) {
|
|
15208
|
+
op.security = security;
|
|
15209
|
+
if (scheme)
|
|
15210
|
+
anySecurity = true;
|
|
15211
|
+
}
|
|
15212
|
+
if (ep.request && ep.request !== "-" && ep.method !== "get") {
|
|
15213
|
+
const requestSchema = resolveSchemaCell(ep.request, schemas);
|
|
15214
|
+
if (requestSchema) {
|
|
15215
|
+
op.requestBody = {
|
|
15216
|
+
description: `Contract request: ${ep.request}`,
|
|
15217
|
+
content: {
|
|
15218
|
+
"application/json": { schema: requestSchema }
|
|
15219
|
+
}
|
|
15220
|
+
};
|
|
15221
|
+
} else {
|
|
15222
|
+
op.requestBody = {
|
|
15223
|
+
description: `Contract request: ${ep.request} (schema not machine-resolved; see contract)`,
|
|
15224
|
+
content: {},
|
|
15225
|
+
"x-cdd-unresolved": true
|
|
15226
|
+
};
|
|
15227
|
+
}
|
|
15228
|
+
}
|
|
15229
|
+
if (ep.response && !responseSchema)
|
|
15230
|
+
op["x-cdd-response-contract"] = ep.response;
|
|
15231
|
+
if (ep.errors)
|
|
15232
|
+
op["x-cdd-errors"] = ep.errors;
|
|
15233
|
+
paths[path] = paths[path] ?? {};
|
|
15234
|
+
paths[path][ep.method] = op;
|
|
15235
|
+
}
|
|
15236
|
+
const doc = {
|
|
15237
|
+
openapi: "3.1.0",
|
|
15238
|
+
info: {
|
|
15239
|
+
title: frontmatter.summary || frontmatter.surface || "API",
|
|
15240
|
+
version: frontmatter["schema-version"] || "0.0.0"
|
|
15241
|
+
},
|
|
15242
|
+
paths,
|
|
15243
|
+
"x-cdd-generated-from": DEFAULT_CONTRACT,
|
|
15244
|
+
"x-cdd-note": "Generated by `cdd-kit openapi export` from the markdown API contract (the source of truth). Partial by design: request/response bodies are free-form prose in the contract and are marked unresolved. Do not hand-edit; regenerate from the contract."
|
|
15245
|
+
};
|
|
15246
|
+
const styleText = styleBlock.trim();
|
|
15247
|
+
if (styleText)
|
|
15248
|
+
doc.info.description = styleText;
|
|
15249
|
+
if (anySecurity) {
|
|
15250
|
+
doc.components = doc.components ?? {};
|
|
15251
|
+
doc.components.securitySchemes = {
|
|
15252
|
+
bearerAuth: { type: "http", scheme: "bearer" }
|
|
15253
|
+
};
|
|
15254
|
+
}
|
|
15255
|
+
if (Object.keys(schemas).length > 0) {
|
|
15256
|
+
doc.components = doc.components ?? {};
|
|
15257
|
+
doc.components.schemas = schemas;
|
|
15258
|
+
}
|
|
15259
|
+
return doc;
|
|
15260
|
+
}
|
|
15261
|
+
function extractStyleBlock(body) {
|
|
15262
|
+
const m = body.match(/^##\s+API Style\s*\n([\s\S]*?)(?:\n##\s|\n#\s|$)/m);
|
|
15263
|
+
if (!m)
|
|
15264
|
+
return "";
|
|
15265
|
+
return m[1].split("\n").map((l) => l.trim()).filter((l) => l.startsWith("-")).join("\n");
|
|
15266
|
+
}
|
|
15267
|
+
function isNonEmptyComposite(v) {
|
|
15268
|
+
if (typeof v !== "object" || v === null)
|
|
15269
|
+
return false;
|
|
15270
|
+
return Array.isArray(v) ? v.length > 0 : Object.keys(v).length > 0;
|
|
15271
|
+
}
|
|
15272
|
+
function yamlScalar(value) {
|
|
15273
|
+
if (value === null || value === void 0)
|
|
15274
|
+
return "null";
|
|
15275
|
+
if (typeof value === "number" || typeof value === "boolean")
|
|
15276
|
+
return String(value);
|
|
15277
|
+
const s = String(value);
|
|
15278
|
+
if (s === "" || /[:#?{}\[\],&*!|>'"%@`]/.test(s) || /^[\s-]/.test(s) || /\s$/.test(s) || /^\d/.test(s)) {
|
|
15279
|
+
return JSON.stringify(s);
|
|
15280
|
+
}
|
|
15281
|
+
return s;
|
|
15282
|
+
}
|
|
15283
|
+
function toYaml(value, indent = 0) {
|
|
15284
|
+
const pad = " ".repeat(indent);
|
|
15285
|
+
if (!isNonEmptyComposite(value)) {
|
|
15286
|
+
if (Array.isArray(value))
|
|
15287
|
+
return "[]";
|
|
15288
|
+
if (typeof value === "object" && value !== null)
|
|
15289
|
+
return "{}";
|
|
15290
|
+
return yamlScalar(value);
|
|
15291
|
+
}
|
|
15292
|
+
if (Array.isArray(value)) {
|
|
15293
|
+
return value.map((item) => {
|
|
15294
|
+
if (isNonEmptyComposite(item)) {
|
|
15295
|
+
return `${pad}-
|
|
15296
|
+
${toYaml(item, indent + 1)}`;
|
|
15297
|
+
}
|
|
15298
|
+
return `${pad}- ${toYaml(item, indent)}`;
|
|
15299
|
+
}).join("\n");
|
|
15300
|
+
}
|
|
15301
|
+
return Object.entries(value).map(([k, v]) => {
|
|
15302
|
+
const key = /[:#\s]/.test(k) ? JSON.stringify(k) : k;
|
|
15303
|
+
if (isNonEmptyComposite(v)) {
|
|
15304
|
+
return `${pad}${key}:
|
|
15305
|
+
${toYaml(v, indent + 1)}`;
|
|
15306
|
+
}
|
|
15307
|
+
return `${pad}${key}: ${toYaml(v, indent)}`;
|
|
15308
|
+
}).join("\n");
|
|
15309
|
+
}
|
|
15310
|
+
async function openapiExport(opts = {}) {
|
|
15311
|
+
const contractPath = opts.contract || DEFAULT_CONTRACT;
|
|
15312
|
+
const format = opts.format || "json";
|
|
15313
|
+
if (!existsSync15(contractPath)) {
|
|
15314
|
+
log.error(`API contract not found: ${contractPath}`);
|
|
15315
|
+
return 1;
|
|
15316
|
+
}
|
|
15317
|
+
const raw = readFileSync14(contractPath, "utf8");
|
|
15318
|
+
const { body, frontmatter } = stripFrontmatter(raw);
|
|
15319
|
+
const endpoints = parseEndpoints(body);
|
|
15320
|
+
const contractSchemas = parseContractSchemas(body);
|
|
15321
|
+
if (contractSchemas.errors.length > 0) {
|
|
15322
|
+
for (const err of contractSchemas.errors)
|
|
15323
|
+
log.error(err);
|
|
15324
|
+
return 1;
|
|
15325
|
+
}
|
|
15326
|
+
if (endpoints.length === 0) {
|
|
15327
|
+
log.error(`No endpoint table rows found in ${contractPath}. Add rows to the "| method | path | ... |" table first.`);
|
|
15328
|
+
return 1;
|
|
15329
|
+
}
|
|
15330
|
+
const styleBlock = extractStyleBlock(body);
|
|
15331
|
+
const doc = buildDoc(endpoints, frontmatter, styleBlock, contractSchemas.schemas);
|
|
15332
|
+
const serialized = format === "yaml" ? `${toYaml(doc)}
|
|
15333
|
+
` : `${JSON.stringify(doc, null, 2)}
|
|
15334
|
+
`;
|
|
15335
|
+
if (opts.check) {
|
|
15336
|
+
if (!opts.out) {
|
|
15337
|
+
log.error("openapi export --check requires --out <path> (the committed artifact to verify against the contract)");
|
|
15338
|
+
return 1;
|
|
15339
|
+
}
|
|
15340
|
+
const contractFlag = contractPath === DEFAULT_CONTRACT ? "" : ` --contract ${contractPath}`;
|
|
15341
|
+
const yamlFlag = format === "yaml" ? " --yaml" : "";
|
|
15342
|
+
const regen = `cdd-kit openapi export${contractFlag} --out ${opts.out}${yamlFlag}`;
|
|
15343
|
+
if (!existsSync15(opts.out)) {
|
|
15344
|
+
log.error(`openapi export --check: ${opts.out} does not exist. Run \`${regen}\` and commit it.`);
|
|
15345
|
+
return 1;
|
|
15346
|
+
}
|
|
15347
|
+
const committed = readFileSync14(opts.out, "utf8");
|
|
15348
|
+
if (committed === serialized) {
|
|
15349
|
+
log.ok(`OpenAPI artifact ${opts.out} is in sync with ${contractPath} (${endpoints.length} endpoint(s))`);
|
|
15350
|
+
return 0;
|
|
15351
|
+
}
|
|
15352
|
+
log.error(`OpenAPI artifact ${opts.out} is OUT OF SYNC with ${contractPath}. The contract changed but the export was not regenerated.`);
|
|
15353
|
+
log.error(`Fix: \`${regen}\` and commit the result.`);
|
|
15354
|
+
return 1;
|
|
15355
|
+
}
|
|
15356
|
+
if (opts.out) {
|
|
15357
|
+
const outPath = resolve2(opts.out);
|
|
15358
|
+
mkdirSync7(dirname4(outPath), { recursive: true });
|
|
15359
|
+
writeFileSync8(outPath, serialized, "utf8");
|
|
15360
|
+
log.ok(`OpenAPI ${format.toUpperCase()} written to ${outPath} (${endpoints.length} endpoint(s))`);
|
|
15361
|
+
const unresolved = countUnresolved(doc);
|
|
15362
|
+
if (unresolved > 0) {
|
|
15363
|
+
log.info(`${unresolved} request body schema(s) left unresolved (free-form prose in the contract). Fill them in the consumer generator or enrich the contract.`);
|
|
15364
|
+
}
|
|
15365
|
+
} else {
|
|
15366
|
+
process.stdout.write(serialized);
|
|
15367
|
+
}
|
|
15368
|
+
return 0;
|
|
15369
|
+
}
|
|
15370
|
+
function countUnresolved(doc) {
|
|
15371
|
+
let n = 0;
|
|
15372
|
+
for (const methods of Object.values(doc.paths)) {
|
|
15373
|
+
for (const op of Object.values(methods)) {
|
|
15374
|
+
if (op.requestBody?.["x-cdd-unresolved"])
|
|
15375
|
+
n += 1;
|
|
15376
|
+
}
|
|
15377
|
+
}
|
|
15378
|
+
return n;
|
|
13794
15379
|
}
|
|
13795
15380
|
|
|
13796
15381
|
// src/cli/index.ts
|
|
13797
|
-
var __dirname2 =
|
|
13798
|
-
var pkg = JSON.parse(
|
|
15382
|
+
var __dirname2 = dirname8(fileURLToPath4(import.meta.url));
|
|
15383
|
+
var pkg = JSON.parse(readFileSync39(join35(__dirname2, "..", "..", "package.json"), "utf8"));
|
|
13799
15384
|
var program = new Command();
|
|
13800
15385
|
program.name("cdd-kit").description("Contract-Driven Delivery Kit CLI").version(pkg.version);
|
|
13801
15386
|
program.command("init").description(
|
|
13802
15387
|
"Install agents/skill into ~/.claude and scaffold project files in cwd"
|
|
13803
|
-
).option("--global-only", "Only install into ~/.claude, skip project files", false).option("--local-only", "Only scaffold project files, skip ~/.claude", false).option("--force", "Overwrite existing project files", false).option("--provider <provider>", "Provider adapter to scaffold: claude, codex, or both", "claude").option("--hooks", "
|
|
15388
|
+
).option("--global-only", "Only install into ~/.claude, skip project files", false).option("--local-only", "Only scaffold project files, skip ~/.claude", false).option("--force", "Overwrite existing project files", false).option("--provider <provider>", "Provider adapter to scaffold: claude, codex, or both", "claude").option("--hooks", "Also install the pre-commit hook that auto-regenerates .cdd/code-map.yml", false).option("--no-arm", "Skip arming enforcement chokepoints (graph-first hook + pre-commit gate)").action(
|
|
13804
15389
|
(opts) => init({
|
|
13805
15390
|
globalOnly: opts.globalOnly,
|
|
13806
15391
|
localOnly: opts.localOnly,
|
|
13807
15392
|
force: opts.force,
|
|
13808
15393
|
provider: opts.provider,
|
|
13809
|
-
hooks: opts.hooks
|
|
15394
|
+
hooks: opts.hooks,
|
|
15395
|
+
arm: opts.arm !== false
|
|
13810
15396
|
})
|
|
13811
15397
|
);
|
|
13812
15398
|
program.command("update").description("Update provider assets for the current project (does not overwrite project guidance files)").option("--yes", "Apply changes (default is dry-run)", false).option("--provider <provider>", "Provider adapter to update: auto, claude, codex, or both", "auto").option("--postinstall", "Internal: invoked by npm postinstall; no-op if cdd has not been init-ed", false).action((opts) => update({ yes: opts.yes, provider: opts.provider, postinstall: opts.postinstall }));
|
|
@@ -13867,7 +15453,26 @@ function resolveWorkers(value) {
|
|
|
13867
15453
|
return 1;
|
|
13868
15454
|
return Math.min(n, 16);
|
|
13869
15455
|
}
|
|
13870
|
-
program.command("code-map [path]").description("Scan source files and emit a structural index at .cdd/code-map.yml").option("--out <path>", "Output YAML path (default .cdd/code-map.yml; with --surface, .cdd/code-map.<surface>.yml)").option("--surface <subpath>", "Scope the scan to a monorepo subtree and name the map after it").option("--workers [n]", "Parallelize JS/TS/Vue scanning across N child processes (default: CPU count - 1)").option("--include <glob>", "Additional include glob (repeatable)", collectRepeatable, []).option("--exclude <glob>", "Additional exclude glob (repeatable)", collectRepeatable, []).option("--check", "Exit 1 if regenerating would change the file (no write)", false).option("--max-lines <n>", "Warn for files exceeding this line count (default 100000)", "100000").action(async (path, opts) => {
|
|
15456
|
+
program.command("code-map [path]").description("Scan source files and emit a structural index at .cdd/code-map.yml").option("--out <path>", "Output YAML path (default .cdd/code-map.yml; with --surface, .cdd/code-map.<surface>.yml)").option("--surface <subpath>", "Scope the scan to a monorepo subtree and name the map after it").option("--workers [n]", "Parallelize JS/TS/Vue scanning across N child processes (default: CPU count - 1)").option("--include <glob>", "Additional include glob (repeatable)", collectRepeatable, []).option("--exclude <glob>", "Additional exclude glob (repeatable)", collectRepeatable, []).option("--check", "Exit 1 if regenerating would change the file (no write)", false).option("--watch", "Keep the map fresh in the background, rebuilding on file changes (debounced)", false).option("--debounce <ms>", "With --watch: coalesce change bursts within this window (default 500)", "500").option("--max-lines <n>", "Warn for files exceeding this line count (default 100000)", "100000").action(async (path, opts) => {
|
|
15457
|
+
if (opts.watch) {
|
|
15458
|
+
const { codeMapWatch: codeMapWatch2 } = await Promise.resolve().then(() => (init_code_map_watch(), code_map_watch_exports));
|
|
15459
|
+
const controller = new AbortController();
|
|
15460
|
+
const onSignal = () => controller.abort();
|
|
15461
|
+
process.once("SIGINT", onSignal);
|
|
15462
|
+
process.once("SIGTERM", onSignal);
|
|
15463
|
+
const exit2 = await codeMapWatch2({
|
|
15464
|
+
path: path ?? ".",
|
|
15465
|
+
out: opts.out,
|
|
15466
|
+
surface: opts.surface,
|
|
15467
|
+
workers: resolveWorkers(opts.workers),
|
|
15468
|
+
include: opts.include,
|
|
15469
|
+
exclude: opts.exclude,
|
|
15470
|
+
maxLines: parseInt(opts.maxLines, 10),
|
|
15471
|
+
debounceMs: parseInt(opts.debounce ?? "500", 10),
|
|
15472
|
+
signal: controller.signal
|
|
15473
|
+
});
|
|
15474
|
+
process.exit(exit2);
|
|
15475
|
+
}
|
|
13871
15476
|
const { codeMap: codeMap2 } = await Promise.resolve().then(() => (init_code_map(), code_map_exports));
|
|
13872
15477
|
const exit = await codeMap2({
|
|
13873
15478
|
path: path ?? ".",
|
|
@@ -13887,13 +15492,15 @@ program.command("__code-map-scan", { hidden: true }).requiredOption("--lang <lan
|
|
|
13887
15492
|
process.exit(exit);
|
|
13888
15493
|
});
|
|
13889
15494
|
var index = program.command("index").description("Query machine-readable project indexes before opening source files");
|
|
13890
|
-
index.command("query <term>").description("Search .cdd/code-map.yml for files, symbols, imports, and line ranges").option("--map <path>", "Code-map YAML path", ".cdd/code-map.yml").option("--limit <n>", "Maximum result files to print", "10").option("--json", "Print machine-readable JSON", false).option("--no-refresh", "Do not auto-regenerate stale or missing code-map before querying").action(async (term, opts) => {
|
|
15495
|
+
index.command("query <term>").description("Search .cdd/code-map.yml for files, symbols, imports, and line ranges").option("--map <path>", "Code-map YAML path", ".cdd/code-map.yml").option("--limit <n>", "Maximum result files to print", "10").option("--json", "Print machine-readable JSON", false).option("--with-source", "Include the matched source slices inline so no separate Read is needed", false).option("--source-budget <n>", "Max total source lines to emit with --with-source", "400").option("--no-refresh", "Do not auto-regenerate stale or missing code-map before querying").action(async (term, opts) => {
|
|
13891
15496
|
const { indexQuery: indexQuery2 } = await Promise.resolve().then(() => (init_index_query(), index_query_exports));
|
|
13892
15497
|
const exit = await indexQuery2(term, {
|
|
13893
15498
|
map: opts.map,
|
|
13894
15499
|
limit: parseInt(opts.limit, 10),
|
|
13895
15500
|
json: opts.json === true,
|
|
13896
|
-
refresh: opts.refresh !== false
|
|
15501
|
+
refresh: opts.refresh !== false,
|
|
15502
|
+
withSource: opts.withSource === true,
|
|
15503
|
+
sourceBudget: parseInt(opts.sourceBudget ?? "400", 10)
|
|
13897
15504
|
});
|
|
13898
15505
|
process.exit(exit);
|
|
13899
15506
|
});
|
|
@@ -13918,14 +15525,16 @@ graph.command("sync [path]").description("Run CodeGraph incremental sync (requir
|
|
|
13918
15525
|
const exit = await graphSync2({ path, engine: opts.engine, json: opts.json === true });
|
|
13919
15526
|
process.exit(exit);
|
|
13920
15527
|
});
|
|
13921
|
-
graph.command("query <term>").description("Search native graph symbols, optionally delegating to CodeGraph or code-map").option("--engine <engine>", "Graph engine: auto, native, codegraph, or codemap", "auto").option("--map <path>", "Code-map YAML path for fallback", ".cdd/code-map.yml").option("--limit <n>", "Maximum results to print", "10").option("--json", "Print machine-readable JSON", false).option("--no-refresh", "Do not auto-regenerate stale or missing fallback code-map").action(async (term, opts) => {
|
|
15528
|
+
graph.command("query <term>").description("Search native graph symbols, optionally delegating to CodeGraph or code-map").option("--engine <engine>", "Graph engine: auto, native, codegraph, or codemap", "auto").option("--map <path>", "Code-map YAML path for fallback", ".cdd/code-map.yml").option("--limit <n>", "Maximum results to print", "10").option("--json", "Print machine-readable JSON", false).option("--with-source", "Include matched source slices inline so no separate Read is needed (native/codemap engines)", false).option("--source-budget <n>", "Max total source lines to emit with --with-source", "400").option("--no-refresh", "Do not auto-regenerate stale or missing fallback code-map").action(async (term, opts) => {
|
|
13922
15529
|
const { graphQuery: graphQuery2 } = await Promise.resolve().then(() => (init_graph(), graph_exports));
|
|
13923
15530
|
const exit = await graphQuery2(term, {
|
|
13924
15531
|
engine: opts.engine,
|
|
13925
15532
|
map: opts.map,
|
|
13926
15533
|
limit: parseInt(opts.limit, 10),
|
|
13927
15534
|
json: opts.json === true,
|
|
13928
|
-
refresh: opts.refresh !== false
|
|
15535
|
+
refresh: opts.refresh !== false,
|
|
15536
|
+
withSource: opts.withSource === true,
|
|
15537
|
+
sourceBudget: parseInt(opts.sourceBudget ?? "400", 10)
|
|
13929
15538
|
});
|
|
13930
15539
|
process.exit(exit);
|
|
13931
15540
|
});
|
|
@@ -13952,6 +15561,11 @@ graph.command("context <task>").description("Build task context with native grap
|
|
|
13952
15561
|
});
|
|
13953
15562
|
process.exit(exit);
|
|
13954
15563
|
});
|
|
15564
|
+
program.command("classify-check [change-id]").description("Show the mechanical risk-tier floor for a change before classification (advisory; gate enforces it)").option("--text <text>", "Scan this inline intent text instead of a change directory").option("--json", "Print machine-readable JSON", false).action(async (changeId, opts) => {
|
|
15565
|
+
const { classifyCheck: classifyCheck2 } = await Promise.resolve().then(() => (init_classify_check(), classify_check_exports));
|
|
15566
|
+
const exit = await classifyCheck2(changeId, { text: opts.text, json: opts.json });
|
|
15567
|
+
process.exit(exit);
|
|
15568
|
+
});
|
|
13955
15569
|
program.command("mcp").description("Run the cdd-kit MCP stdio server exposing graph and code-map tools").action(async () => {
|
|
13956
15570
|
const { runMcpServer: runMcpServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
|
|
13957
15571
|
await runMcpServer2({ version: pkg.version });
|
|
@@ -13983,6 +15597,19 @@ program.command("list").description("List active changes in specs/changes/").act
|
|
|
13983
15597
|
program.command("install-hooks").description("Install pre-commit hook that runs cdd-kit gate on staged changes").action(async () => {
|
|
13984
15598
|
await installHooks();
|
|
13985
15599
|
});
|
|
15600
|
+
program.command("install-agent-hooks").description("Install Claude Code agent hooks into .claude/settings.json (graph-first exploration)").option("--graph-first <mode>", "Install the graph-first PreToolUse hook: 'advisory' (default) or 'strict'", "advisory").action(async (opts) => {
|
|
15601
|
+
await installAgentHooks({ graphFirst: opts.graphFirst });
|
|
15602
|
+
});
|
|
15603
|
+
var openapi = program.command("openapi").description("Project the API contract into tooling artifacts (see docs/adr/0001-contract-to-openapi-export.md)");
|
|
15604
|
+
openapi.command("export").description("Export contracts/api/api-contract.md as a minimal OpenAPI 3.1 skeleton").option("--contract <path>", "API contract markdown path", "contracts/api/api-contract.md").option("--out <path>", "Write to a file instead of stdout").option("--yaml", "Emit YAML instead of JSON", false).option("--check", "Verify the artifact at --out is in sync with the contract (exits 1 on drift); does not write", false).action(async (opts) => {
|
|
15605
|
+
const exit = await openapiExport({
|
|
15606
|
+
contract: opts.contract,
|
|
15607
|
+
out: opts.out,
|
|
15608
|
+
format: opts.yaml ? "yaml" : "json",
|
|
15609
|
+
check: opts.check
|
|
15610
|
+
});
|
|
15611
|
+
process.exit(exit);
|
|
15612
|
+
});
|
|
13986
15613
|
program.command("detect-stack").description("Detect the project tech stack and print the result").action(() => {
|
|
13987
15614
|
const cwd = process.cwd();
|
|
13988
15615
|
const result = detectStack(cwd);
|