contract-driven-delivery 2.0.7 → 2.0.9

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/dist/cli/index.js +2328 -1903
  3. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -139,6 +139,13 @@ ${CODE_MAP_BLOCK}
139
139
  chmodSync(dest, 493);
140
140
  } catch {
141
141
  }
142
+ try {
143
+ const cddDir = join4(cwd, ".cdd");
144
+ mkdirSync2(cddDir, { recursive: true });
145
+ writeFileSync(join4(cddDir, ".hooks-installed"), `installed: ${(/* @__PURE__ */ new Date()).toISOString()}
146
+ `, "utf8");
147
+ } catch {
148
+ }
142
149
  log.ok(`code-map pre-commit hook installed at ${dest}`);
143
150
  }
144
151
  var START_MARKER, END_MARKER, CODE_MAP_BLOCK;
@@ -151,7 +158,8 @@ var init_code_map_hook = __esm({
151
158
  CODE_MAP_BLOCK = `${START_MARKER}
152
159
  # Auto-regenerates .cdd/code-map.yml when source files are staged.
153
160
  # Generated by: cdd-kit init --hooks (re-run to update).
154
- staged_src=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\\.(py|js|vue)$' || true)
161
+ # Extension list mirrors src/code-map/config.ts BUILTIN_INCLUDE.
162
+ staged_src=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\\.(py|js|jsx|mjs|cjs|ts|tsx|vue)$' || true)
155
163
  if [ -n "$staged_src" ]; then
156
164
  if cdd-kit code-map --check >/dev/null 2>&1; then
157
165
  : # already up to date
@@ -201,6 +209,173 @@ var init_provider = __esm({
201
209
  }
202
210
  });
203
211
 
212
+ // src/commands/update.ts
213
+ import { join as join7 } from "path";
214
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readdirSync as readdirSync3, copyFileSync as copyFileSync2, readFileSync as readFileSync5 } from "fs";
215
+ import { createHash } from "crypto";
216
+ import { homedir as homedir2 } from "os";
217
+ function fileHash(filePath) {
218
+ const buf = readFileSync5(filePath);
219
+ return createHash("sha256").update(buf).digest("hex");
220
+ }
221
+ function diffDir(src, dest) {
222
+ const entries = [];
223
+ if (!existsSync6(src))
224
+ return entries;
225
+ function walk(currentSrc, currentDest) {
226
+ const items = readdirSync3(currentSrc, { withFileTypes: true });
227
+ for (const item of items) {
228
+ const srcPath = join7(currentSrc, item.name);
229
+ const destPath = join7(currentDest, item.name);
230
+ if (item.isDirectory()) {
231
+ walk(srcPath, destPath);
232
+ } else {
233
+ if (!existsSync6(destPath)) {
234
+ entries.push({ src: srcPath, dest: destPath, action: "add" });
235
+ } else if (fileHash(srcPath) !== fileHash(destPath)) {
236
+ entries.push({ src: srcPath, dest: destPath, action: "overwrite" });
237
+ } else {
238
+ entries.push({ src: srcPath, dest: destPath, action: "skip" });
239
+ }
240
+ }
241
+ }
242
+ }
243
+ walk(src, dest);
244
+ return entries;
245
+ }
246
+ function applyDir(entries) {
247
+ let count = 0;
248
+ for (const e of entries) {
249
+ if (e.action === "skip")
250
+ continue;
251
+ mkdirSync3(join7(e.dest, ".."), { recursive: true });
252
+ copyFileSync2(e.src, e.dest);
253
+ count += 1;
254
+ }
255
+ return count;
256
+ }
257
+ function backupDir(dir, backupDest) {
258
+ if (!existsSync6(dir))
259
+ return;
260
+ mkdirSync3(backupDest, { recursive: true });
261
+ function walk(src, dst) {
262
+ const items = readdirSync3(src, { withFileTypes: true });
263
+ for (const item of items) {
264
+ const s = join7(src, item.name);
265
+ const d = join7(dst, item.name);
266
+ if (item.isDirectory()) {
267
+ mkdirSync3(d, { recursive: true });
268
+ walk(s, d);
269
+ } else
270
+ copyFileSync2(s, d);
271
+ }
272
+ }
273
+ walk(dir, backupDest);
274
+ }
275
+ async function update(opts) {
276
+ if (opts.postinstall) {
277
+ if (!existsSync6(join7(SKILLS_HOME, "contract-driven-delivery"))) {
278
+ return;
279
+ }
280
+ opts.yes = true;
281
+ opts.provider = "claude";
282
+ }
283
+ const quiet = !!opts.postinstall;
284
+ if (!quiet)
285
+ log.blank();
286
+ const cwd = process.cwd();
287
+ const requestedProvider = opts.provider ?? "auto";
288
+ if (!validateProviderOption(requestedProvider)) {
289
+ log.error(`Invalid provider: ${requestedProvider}. Use auto, claude, codex, or both.`);
290
+ process.exit(1);
291
+ }
292
+ const provider = inferProvider(cwd, requestedProvider);
293
+ const updateClaudeAssets = provider === "claude" || provider === "both";
294
+ const agentDiff = updateClaudeAssets ? diffDir(ASSET.agents, AGENTS_HOME) : [];
295
+ const skillDiff = updateClaudeAssets ? readdirSync3(ASSET.skills, { withFileTypes: true }).filter((d) => d.isDirectory()).flatMap((d) => diffDir(join7(ASSET.skills, d.name), join7(SKILLS_HOME, d.name))) : [];
296
+ const toWrite = [...agentDiff, ...skillDiff].filter((e) => e.action !== "skip");
297
+ const toAdd = toWrite.filter((e) => e.action === "add");
298
+ const toOver = toWrite.filter((e) => e.action === "overwrite");
299
+ const toSkip = [...agentDiff, ...skillDiff].filter((e) => e.action === "skip");
300
+ if (!quiet) {
301
+ log.info(`Provider: ${provider}`);
302
+ if (updateClaudeAssets) {
303
+ log.info(`Dry-run diff \u2014 agents: ${AGENTS_HOME}`);
304
+ log.info(`Dry-run diff \u2014 skills: ${SKILLS_HOME}`);
305
+ } else {
306
+ log.info("Codex provider has no global cdd-kit assets to update.");
307
+ log.info("Project files are preserved; run cdd-kit init --local-only --provider codex to add missing local guidance.");
308
+ }
309
+ log.blank();
310
+ if (toAdd.length)
311
+ log.info(` + ${toAdd.length} file(s) would be added`);
312
+ if (toOver.length)
313
+ log.warn(` ~ ${toOver.length} file(s) would be overwritten (user edits lost without backup)`);
314
+ if (toSkip.length)
315
+ log.dim(` ${toSkip.length} file(s) unchanged (skipped)`);
316
+ }
317
+ if (toWrite.length === 0) {
318
+ if (!quiet) {
319
+ log.blank();
320
+ log.ok("Already up to date \u2014 nothing to write.");
321
+ log.blank();
322
+ }
323
+ return;
324
+ }
325
+ if (!quiet && !opts.yes) {
326
+ log.blank();
327
+ log.info("Run with --yes to apply changes. Example:");
328
+ log.dim(" cdd-kit update --yes");
329
+ log.blank();
330
+ return;
331
+ }
332
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
333
+ const backupRoot = join7(homedir2(), ".claude", ".cdd-kit-backup", timestamp);
334
+ if (!quiet) {
335
+ log.blank();
336
+ log.info(`Backing up to ${backupRoot} \u2026`);
337
+ }
338
+ backupDir(AGENTS_HOME, join7(backupRoot, "agents"));
339
+ backupDir(SKILLS_HOME, join7(backupRoot, "skills"));
340
+ if (!quiet)
341
+ log.ok(`Backup complete: ${backupRoot}`);
342
+ if (!quiet)
343
+ log.blank();
344
+ let totalSynced = 0;
345
+ if (updateClaudeAssets) {
346
+ if (!quiet)
347
+ log.info(`Updating agents \u2192 ${AGENTS_HOME}`);
348
+ const agentCount = applyDir(agentDiff);
349
+ if (!quiet)
350
+ log.ok(`${agentCount} agent file(s) updated.`);
351
+ totalSynced += agentCount;
352
+ if (!quiet)
353
+ log.info(`Updating skills \u2192 ${SKILLS_HOME}`);
354
+ const skillCount = applyDir(skillDiff);
355
+ if (!quiet)
356
+ log.ok(`${skillCount} skill file(s) updated.`);
357
+ totalSynced += skillCount;
358
+ }
359
+ if (quiet) {
360
+ if (totalSynced > 0)
361
+ log.ok(`cdd-kit: synced ${totalSynced} file(s) to ~/.claude/`);
362
+ } else {
363
+ log.blank();
364
+ log.info("Project files (contracts/, specs/, tests/, ci/) were not changed.");
365
+ log.ok("Update complete.");
366
+ log.info(`Backup saved to: ${backupRoot}`);
367
+ log.blank();
368
+ }
369
+ }
370
+ var init_update = __esm({
371
+ "src/commands/update.ts"() {
372
+ "use strict";
373
+ init_paths();
374
+ init_logger();
375
+ init_provider();
376
+ }
377
+ });
378
+
204
379
  // src/commands/context-scan.ts
205
380
  var context_scan_exports = {};
206
381
  __export(context_scan_exports, {
@@ -240,7 +415,14 @@ function getForbiddenPaths(cwd) {
240
415
  }
241
416
  function isForbidden(relPath, forbidden) {
242
417
  const normalized = relPath.replace(/\\/g, "/");
243
- return forbidden.some((pattern) => normalized === pattern || normalized.startsWith(`${pattern}/`));
418
+ if (forbidden.some((pattern) => normalized === pattern || normalized.startsWith(`${pattern}/`))) {
419
+ return true;
420
+ }
421
+ for (const segment of normalized.split("/")) {
422
+ if (FORBIDDEN_DIRECTORY_NAMES.has(segment))
423
+ return true;
424
+ }
425
+ return false;
244
426
  }
245
427
  function buildTree(dir, cwd, forbidden, stats, prefix = "", depth = 0) {
246
428
  const entries = readdirSync4(dir, { withFileTypes: true }).sort((a, b) => {
@@ -457,7 +639,7 @@ async function contextScan(opts = {}) {
457
639
  log.ok("Created specs/context/contracts-index.md");
458
640
  }
459
641
  }
460
- var DEFAULT_FORBIDDEN, PER_DIR_ENTRY_CAP;
642
+ var DEFAULT_FORBIDDEN, FORBIDDEN_DIRECTORY_NAMES, PER_DIR_ENTRY_CAP;
461
643
  var init_context_scan = __esm({
462
644
  "src/commands/context-scan.ts"() {
463
645
  "use strict";
@@ -470,8 +652,43 @@ var init_context_scan = __esm({
470
652
  "build",
471
653
  "assets",
472
654
  "specs/archive",
473
- "specs/changes"
655
+ "specs/changes",
656
+ // cdd-kit runtime artifacts. Without these the user's local backups land
657
+ // in `specs/context/project-map.md`, polluting it and breaking the
658
+ // inputs-digest match for any fresh clone.
659
+ ".cdd/.refresh-backup",
660
+ ".cdd/migrate-backup",
661
+ ".cdd/runtime"
474
662
  ];
663
+ FORBIDDEN_DIRECTORY_NAMES = /* @__PURE__ */ new Set([
664
+ // Node — also caught by top-level prefix, but a nested
665
+ // `frontend/node_modules` requires basename matching
666
+ "node_modules",
667
+ // Python
668
+ "__pycache__",
669
+ ".pytest_cache",
670
+ ".mypy_cache",
671
+ ".ruff_cache",
672
+ ".tox",
673
+ // JS/TS frameworks
674
+ ".next",
675
+ ".nuxt",
676
+ ".svelte-kit",
677
+ ".parcel-cache",
678
+ ".turbo",
679
+ ".nyc_output",
680
+ // Generic build / coverage caches
681
+ "coverage",
682
+ "htmlcov",
683
+ ".cache",
684
+ // Virtualenvs
685
+ "venv",
686
+ ".venv",
687
+ // IDE / OS noise
688
+ ".idea",
689
+ ".vscode",
690
+ ".DS_Store"
691
+ ]);
475
692
  PER_DIR_ENTRY_CAP = 50;
476
693
  }
477
694
  });
@@ -4046,49 +4263,49 @@ var require_fast_uri = __commonJS({
4046
4263
  schemelessOptions.skipEscape = true;
4047
4264
  return serialize(resolved, schemelessOptions);
4048
4265
  }
4049
- function resolveComponent(base, relative5, options, skipNormalization) {
4266
+ function resolveComponent(base, relative6, options, skipNormalization) {
4050
4267
  const target = {};
4051
4268
  if (!skipNormalization) {
4052
4269
  base = parse3(serialize(base, options), options);
4053
- relative5 = parse3(serialize(relative5, options), options);
4270
+ relative6 = parse3(serialize(relative6, options), options);
4054
4271
  }
4055
4272
  options = options || {};
4056
- if (!options.tolerant && relative5.scheme) {
4057
- target.scheme = relative5.scheme;
4058
- target.userinfo = relative5.userinfo;
4059
- target.host = relative5.host;
4060
- target.port = relative5.port;
4061
- target.path = removeDotSegments(relative5.path || "");
4062
- target.query = relative5.query;
4273
+ if (!options.tolerant && relative6.scheme) {
4274
+ target.scheme = relative6.scheme;
4275
+ target.userinfo = relative6.userinfo;
4276
+ target.host = relative6.host;
4277
+ target.port = relative6.port;
4278
+ target.path = removeDotSegments(relative6.path || "");
4279
+ target.query = relative6.query;
4063
4280
  } else {
4064
- if (relative5.userinfo !== void 0 || relative5.host !== void 0 || relative5.port !== void 0) {
4065
- target.userinfo = relative5.userinfo;
4066
- target.host = relative5.host;
4067
- target.port = relative5.port;
4068
- target.path = removeDotSegments(relative5.path || "");
4069
- target.query = relative5.query;
4281
+ if (relative6.userinfo !== void 0 || relative6.host !== void 0 || relative6.port !== void 0) {
4282
+ target.userinfo = relative6.userinfo;
4283
+ target.host = relative6.host;
4284
+ target.port = relative6.port;
4285
+ target.path = removeDotSegments(relative6.path || "");
4286
+ target.query = relative6.query;
4070
4287
  } else {
4071
- if (!relative5.path) {
4288
+ if (!relative6.path) {
4072
4289
  target.path = base.path;
4073
- if (relative5.query !== void 0) {
4074
- target.query = relative5.query;
4290
+ if (relative6.query !== void 0) {
4291
+ target.query = relative6.query;
4075
4292
  } else {
4076
4293
  target.query = base.query;
4077
4294
  }
4078
4295
  } else {
4079
- if (relative5.path[0] === "/") {
4080
- target.path = removeDotSegments(relative5.path);
4296
+ if (relative6.path[0] === "/") {
4297
+ target.path = removeDotSegments(relative6.path);
4081
4298
  } else {
4082
4299
  if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) {
4083
- target.path = "/" + relative5.path;
4300
+ target.path = "/" + relative6.path;
4084
4301
  } else if (!base.path) {
4085
- target.path = relative5.path;
4302
+ target.path = relative6.path;
4086
4303
  } else {
4087
- target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative5.path;
4304
+ target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative6.path;
4088
4305
  }
4089
4306
  target.path = removeDotSegments(target.path);
4090
4307
  }
4091
- target.query = relative5.query;
4308
+ target.query = relative6.query;
4092
4309
  }
4093
4310
  target.userinfo = base.userinfo;
4094
4311
  target.host = base.host;
@@ -4096,7 +4313,7 @@ var require_fast_uri = __commonJS({
4096
4313
  }
4097
4314
  target.scheme = base.scheme;
4098
4315
  }
4099
- target.fragment = relative5.fragment;
4316
+ target.fragment = relative6.fragment;
4100
4317
  return target;
4101
4318
  }
4102
4319
  function equal(uriA, uriB, options) {
@@ -7468,1877 +7685,2232 @@ var init_freshness = __esm({
7468
7685
  }
7469
7686
  });
7470
7687
 
7471
- // src/code-map/yaml-writer.ts
7472
- function quoteScalar(s) {
7473
- if (s.startsWith(".") || YAML_RESERVED.test(s) || s.includes(" ") || s === "" || s === "null" || s === "true" || s === "false") {
7474
- return `'${s.replace(/'/g, "''")}'`;
7475
- }
7476
- return s;
7477
- }
7478
- function quotePath(p) {
7479
- if (YAML_RESERVED.test(p) || p.includes(" ")) {
7480
- return `'${p.replace(/'/g, "''")}'`;
7481
- }
7482
- return p;
7483
- }
7484
- function renderItems(items) {
7485
- if (items.length === 0)
7486
- return "[]";
7487
- return `[${items.map(quoteScalar).join(", ")}]`;
7488
- }
7489
- function truncateDecorator(d, max = 80) {
7490
- const cleaned = d.replace(/\r?\n/g, " ");
7491
- return cleaned.length <= max ? cleaned : cleaned.slice(0, max) + "...";
7492
- }
7493
- function renderClass(c) {
7494
- const lines = [];
7495
- lines.push(` - name: ${c.name}`);
7496
- lines.push(` lines: ${c.lines[0]}-${c.lines[1]}`);
7497
- if (c.methods.length > 0) {
7498
- lines.push(" methods:");
7499
- for (const m of c.methods) {
7500
- const asyncPrefix = m.async ? "async " : "";
7501
- lines.push(` - { name: ${asyncPrefix}${m.name}, lines: ${m.lines[0]}-${m.lines[1]} }`);
7502
- }
7688
+ // src/commands/migrate.ts
7689
+ var migrate_exports = {};
7690
+ __export(migrate_exports, {
7691
+ migrate: () => migrate
7692
+ });
7693
+ import { join as join16 } from "path";
7694
+ import { cpSync as cpSync2, existsSync as existsSync14, mkdirSync as mkdirSync6, readdirSync as readdirSync8, readFileSync as readFileSync11, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
7695
+ import yaml3 from "js-yaml";
7696
+ function backupChangeDir(cwd, changeId, sessionStamp) {
7697
+ const backupRoot = join16(cwd, ".cdd", "migrate-backup", sessionStamp);
7698
+ const backupDir2 = join16(backupRoot, changeId);
7699
+ mkdirSync6(backupRoot, { recursive: true });
7700
+ const sourceDir = join16(cwd, "specs", "changes", changeId);
7701
+ if (existsSync14(sourceDir)) {
7702
+ cpSync2(sourceDir, backupDir2, { recursive: true });
7503
7703
  }
7504
- return lines;
7704
+ return backupDir2;
7505
7705
  }
7506
- function renderFunction(f) {
7507
- const asyncPrefix = f.async ? "async " : "";
7508
- let comment = "";
7509
- if (f.decorators.length > 0) {
7510
- const truncated = truncateDecorator(f.decorators[0]);
7511
- comment = ` # @${truncated}`;
7512
- }
7513
- return ` - { name: ${asyncPrefix}${f.name}, lines: ${f.lines[0]}-${f.lines[1]} }${comment}`;
7706
+ function buildLegacyContextManifest(changeId) {
7707
+ return [
7708
+ "# Context Manifest",
7709
+ "",
7710
+ "Generated by `cdd-kit migrate` for an existing change.",
7711
+ "Legacy manifest. Forbidden paths come from `.cdd/context-policy.json`.",
7712
+ "",
7713
+ "## Affected Surfaces",
7714
+ "- legacy-unknown",
7715
+ "",
7716
+ "## Allowed Paths",
7717
+ `- specs/changes/${changeId}/`,
7718
+ "",
7719
+ "## Required Contracts",
7720
+ "- legacy-unknown",
7721
+ "",
7722
+ "## Required Tests",
7723
+ "- legacy-unknown",
7724
+ "",
7725
+ "## Agent Work Packets",
7726
+ "",
7727
+ "## Context Expansion Requests",
7728
+ "-",
7729
+ "",
7730
+ "## Approved Expansions",
7731
+ "-",
7732
+ ""
7733
+ ].join("\n");
7514
7734
  }
7515
- function renderTypeDef(t) {
7516
- const exportedSuffix = t.exported ? "" : " # local";
7517
- return ` - { name: ${t.name}, lines: ${t.lines[0]}-${t.lines[1]} }${exportedSuffix}`;
7735
+ function buildContextGovernedManifest(changeId) {
7736
+ return [
7737
+ "# Context Manifest",
7738
+ "",
7739
+ "Generated by `cdd-kit migrate --enable-context-governance` for an existing change.",
7740
+ "Review and narrow the allowed paths before assigning implementation work.",
7741
+ "Forbidden paths come from `.cdd/context-policy.json`.",
7742
+ "",
7743
+ "## Affected Surfaces",
7744
+ "- legacy-unknown",
7745
+ "",
7746
+ "## Allowed Paths",
7747
+ `- specs/changes/${changeId}/`,
7748
+ "- specs/context/project-map.md",
7749
+ "- specs/context/contracts-index.md",
7750
+ "",
7751
+ "## Required Contracts",
7752
+ "- legacy-unknown",
7753
+ "",
7754
+ "## Required Tests",
7755
+ "- legacy-unknown",
7756
+ "",
7757
+ "## Agent Work Packets",
7758
+ "",
7759
+ "### change-classifier",
7760
+ "- allowed:",
7761
+ ` - specs/changes/${changeId}/`,
7762
+ " - specs/context/project-map.md",
7763
+ " - specs/context/contracts-index.md",
7764
+ "",
7765
+ "## Context Expansion Requests",
7766
+ "",
7767
+ "<!--",
7768
+ "Agents must request context expansion instead of reading outside their work packet.",
7769
+ "Use this format only for real requests:",
7770
+ "",
7771
+ "- request-id: CER-001",
7772
+ " requested_paths:",
7773
+ " - src/example.ts",
7774
+ " reason: why this file is required",
7775
+ " status: pending",
7776
+ "-->",
7777
+ "",
7778
+ "## Approved Expansions",
7779
+ "-",
7780
+ ""
7781
+ ].join("\n");
7518
7782
  }
7519
- function renderEnum(e) {
7520
- const lines = [];
7521
- const exportedSuffix = e.exported ? "" : " # local";
7522
- lines.push(` - name: ${e.name}`);
7523
- lines.push(` lines: ${e.lines[0]}-${e.lines[1]}${exportedSuffix}`);
7524
- if (e.members.length > 0) {
7525
- lines.push(` members: [${e.members.join(", ")}]`);
7783
+ function parseLegacyFrontmatter(content) {
7784
+ const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
7785
+ if (!m)
7786
+ return {};
7787
+ const out = {};
7788
+ for (const line of m[1].split(/\r?\n/)) {
7789
+ const colon = line.indexOf(":");
7790
+ if (colon === -1)
7791
+ continue;
7792
+ const key = line.slice(0, colon).trim();
7793
+ if (!key)
7794
+ continue;
7795
+ out[key] = line.slice(colon + 1).trim();
7526
7796
  }
7527
- return lines;
7797
+ return out;
7528
7798
  }
7529
- function renderYaml(entries, opts) {
7530
- const now = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "Z");
7531
- const totalSrc = entries.reduce((s, e) => s + e.total_lines, 0);
7532
- const bodyLines = [];
7533
- for (let i = 0; i < entries.length; i++) {
7534
- const e = entries[i];
7535
- if (i > 0)
7536
- bodyLines.push("");
7537
- const pathKey = quotePath(e.path);
7538
- bodyLines.push(`${pathKey}: # ${e.total_lines} lines`);
7539
- if (e.imports.length > 0) {
7540
- bodyLines.push(" imports:");
7541
- for (const imp of e.imports) {
7542
- const mod = quoteScalar(imp.module);
7543
- bodyLines.push(` - { module: ${mod}, items: ${renderItems(imp.items)}, line: ${imp.line} }`);
7544
- }
7545
- }
7546
- if (e.constants.length > 0) {
7547
- bodyLines.push(" constants:");
7548
- for (const c of e.constants) {
7549
- bodyLines.push(` - { name: ${c.name}, line: ${c.line} }`);
7550
- }
7551
- }
7552
- if (e.classes.length > 0) {
7553
- bodyLines.push(" classes:");
7554
- for (const c of e.classes) {
7555
- bodyLines.push(...renderClass(c));
7556
- }
7557
- }
7558
- if (e.functions.length > 0) {
7559
- bodyLines.push(" functions:");
7560
- for (const f of e.functions) {
7561
- bodyLines.push(renderFunction(f));
7562
- }
7563
- }
7564
- if (e.interfaces && e.interfaces.length > 0) {
7565
- bodyLines.push(" interfaces:");
7566
- for (const t of e.interfaces) {
7567
- bodyLines.push(renderTypeDef(t));
7568
- }
7569
- }
7570
- if (e.types && e.types.length > 0) {
7571
- bodyLines.push(" types:");
7572
- for (const t of e.types) {
7573
- bodyLines.push(renderTypeDef(t));
7574
- }
7575
- }
7576
- if (e.enums && e.enums.length > 0) {
7577
- bodyLines.push(" enums:");
7578
- for (const en of e.enums) {
7579
- bodyLines.push(...renderEnum(en));
7580
- }
7581
- }
7582
- }
7583
- const mapLines = bodyLines.length + 2;
7584
- const compression = totalSrc === 0 ? 0 : totalSrc / mapLines;
7585
- const fileCount = entries.length;
7586
- const header = [
7587
- `# generated: ${now} by ${opts.generator}`,
7588
- `# files: ${fileCount}, src-lines: ${totalSrc}, map-lines: ${mapLines}, compression: ${compression.toFixed(1)}x`
7589
- ];
7590
- return [...header, ...bodyLines].join("\n") + "\n";
7799
+ function parseListField(raw) {
7800
+ if (!raw)
7801
+ return [];
7802
+ const trimmed = raw.trim();
7803
+ if (!trimmed || trimmed === "[]")
7804
+ return [];
7805
+ const inner = trimmed.startsWith("[") && trimmed.endsWith("]") ? trimmed.slice(1, -1) : trimmed;
7806
+ return inner.split(",").map((item) => item.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
7591
7807
  }
7592
- var YAML_RESERVED;
7593
- var init_yaml_writer = __esm({
7594
- "src/code-map/yaml-writer.ts"() {
7595
- "use strict";
7596
- YAML_RESERVED = /[:[\]{},&*!|>'"%@`#]/;
7597
- }
7598
- });
7599
-
7600
- // src/code-map/orchestrator.ts
7601
- async function scanInProcess(scanner, absolutePaths, repoRoot) {
7602
- const entries = [];
7603
- const warnings = [];
7604
- for (const absPath of absolutePaths) {
7605
- try {
7606
- const entry = await scanner.scan(absPath, repoRoot);
7607
- if (entry === null) {
7608
- const rel = absPath.replace(/\\/g, "/").replace(repoRoot.replace(/\\/g, "/") + "/", "");
7609
- warnings.push({ path: rel, message: "parse error (scanner returned null)" });
7610
- } else {
7611
- entries.push(entry);
7612
- }
7613
- } catch (err) {
7614
- const rel = absPath.replace(/\\/g, "/").replace(repoRoot.replace(/\\/g, "/") + "/", "");
7615
- warnings.push({ path: rel, message: `IO error: ${err.message}` });
7808
+ function parseLegacyTaskList(body) {
7809
+ const lines = body.split(/\r?\n/);
7810
+ const rows = [];
7811
+ let currentSection;
7812
+ for (const raw of lines) {
7813
+ const headerMatch = raw.match(/^##\s+\d+\.\s+(.*)\s*$/);
7814
+ if (headerMatch) {
7815
+ currentSection = headerMatch[1].trim();
7816
+ continue;
7616
7817
  }
7818
+ const itemMatch = raw.match(/^\s*-\s*\[([ xX\-])\]\s+(\d+(?:\.\d+)*)\s+(.*)\s*$/);
7819
+ if (!itemMatch)
7820
+ continue;
7821
+ const mark = itemMatch[1];
7822
+ const id = itemMatch[2];
7823
+ const title = itemMatch[3].trim();
7824
+ let status = "pending";
7825
+ if (mark === "x" || mark === "X")
7826
+ status = "done";
7827
+ else if (mark === "-")
7828
+ status = "skipped";
7829
+ rows.push({ id, title, status, section: currentSection });
7617
7830
  }
7618
- return { entries, warnings };
7831
+ return rows;
7619
7832
  }
7620
- function bucketByExtension(files) {
7621
- const out = {};
7622
- for (const f of files) {
7623
- const dot = f.lastIndexOf(".");
7624
- const ext = dot >= 0 ? f.slice(dot).toLowerCase() : "";
7625
- if (!out[ext])
7626
- out[ext] = [];
7627
- out[ext].push(f);
7833
+ function migrateTasksFile(changeId, changeDir, enableContextGovernance, detectedTier, changed, warnings, pendingWrites, pendingDeletes) {
7834
+ const newPath = join16(changeDir, "tasks.yml");
7835
+ const legacyPath = join16(changeDir, "tasks.md");
7836
+ if (existsSync14(newPath)) {
7837
+ return;
7628
7838
  }
7629
- return out;
7630
- }
7631
- var init_orchestrator = __esm({
7632
- "src/code-map/orchestrator.ts"() {
7633
- "use strict";
7634
- init_include_exclude();
7839
+ if (!existsSync14(legacyPath)) {
7840
+ warnings.push("tasks.md not found and tasks.yml missing \u2014 skipping tasks migration");
7841
+ return;
7635
7842
  }
7636
- });
7637
-
7638
- // src/code-map/scanners/common.ts
7639
- import { relative as relative3 } from "path";
7640
- function canonicalRelPath(absolutePath, repoRoot) {
7641
- const rel = relative3(repoRoot, absolutePath);
7642
- return rel.replace(/\\/g, "/").normalize("NFC");
7643
- }
7644
- function isAllCapsConst(name) {
7645
- return name.length >= 2 && /^[A-Z][A-Z0-9_]*$/.test(name);
7646
- }
7647
- function isBinary(content) {
7648
- const head = content.slice(0, 4096);
7649
- return head.includes("\0");
7650
- }
7651
- var init_common = __esm({
7652
- "src/code-map/scanners/common.ts"() {
7653
- "use strict";
7843
+ const raw = readFileSync11(legacyPath, "utf8");
7844
+ const fm = parseLegacyFrontmatter(raw);
7845
+ const bodyMatch = raw.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?([\s\S]*)$/);
7846
+ const body = bodyMatch ? bodyMatch[1] : raw;
7847
+ const tasksRows = parseLegacyTaskList(body);
7848
+ const data = {};
7849
+ data["change-id"] = fm["change-id"] || changeId;
7850
+ data["status"] = fm["status"] || "in-progress";
7851
+ if (fm["tier"] && /^\d+$/.test(fm["tier"])) {
7852
+ data["tier"] = parseInt(fm["tier"], 10);
7853
+ } else if (detectedTier) {
7854
+ data["tier"] = parseInt(detectedTier, 10);
7855
+ } else {
7856
+ data["tier"] = null;
7654
7857
  }
7655
- });
7656
-
7657
- // src/code-map/scanners/python.ts
7658
- var python_exports = {};
7659
- __export(python_exports, {
7660
- pythonScanner: () => pythonScanner
7661
- });
7662
- import { spawnSync as spawnSync2 } from "child_process";
7663
- import { writeFileSync as writeFileSync6, unlinkSync } from "fs";
7664
- import { join as join16 } from "path";
7665
- import { tmpdir } from "os";
7666
- function detectPython2() {
7667
- for (const candidate of ["python3", "python"]) {
7668
- try {
7669
- const result = spawnSync2(candidate, ["--version"], {
7670
- encoding: "utf8",
7671
- timeout: 5e3
7672
- });
7673
- if (result.status === 0)
7674
- return candidate;
7675
- } catch {
7676
- }
7858
+ if (enableContextGovernance || fm["context-governance"] === "v1") {
7859
+ data["context-governance"] = "v1";
7677
7860
  }
7678
- return null;
7861
+ const archive2 = parseListField(fm["archive-tasks"]);
7862
+ data["archive-tasks"] = archive2.length > 0 ? archive2 : ["7.1", "7.2"];
7863
+ const deps = parseListField(fm["depends-on"]);
7864
+ data["depends-on"] = deps;
7865
+ data["tasks"] = tasksRows.map((r) => {
7866
+ const out = { id: r.id, title: r.title, status: r.status };
7867
+ if (r.section)
7868
+ out["section"] = r.section;
7869
+ return out;
7870
+ });
7871
+ const yamlOut = yaml3.dump(data, { lineWidth: -1, noRefs: true });
7872
+ pendingWrites.push({ path: newPath, content: yamlOut });
7873
+ pendingDeletes.push({ path: legacyPath });
7874
+ changed.push(`tasks.md -> tasks.yml (${tasksRows.length} task(s) migrated)`);
7679
7875
  }
7680
- var TIMEOUT_MS, PythonScanner, pythonScanner;
7681
- var init_python = __esm({
7682
- "src/code-map/scanners/python.ts"() {
7683
- "use strict";
7684
- init_paths();
7685
- init_common();
7686
- TIMEOUT_MS = parseInt(process.env["CDD_CODE_MAP_TIMEOUT_MS"] ?? "30000", 10);
7687
- PythonScanner = class {
7688
- extensions = [".py"];
7689
- _interpreter = void 0;
7690
- getInterpreter() {
7691
- if (this._interpreter === void 0) {
7692
- this._interpreter = detectPython2();
7876
+ function parseLegacyAgentLog(content) {
7877
+ const lines = content.split(/\r?\n/);
7878
+ const data = {};
7879
+ let i = 0;
7880
+ const topFieldRe = /^[ ]{0,1}-\s*([\w-]+):\s*(.*)$/;
7881
+ while (i < lines.length) {
7882
+ const line = lines[i];
7883
+ const fieldMatch = line.match(topFieldRe);
7884
+ if (!fieldMatch) {
7885
+ i++;
7886
+ continue;
7887
+ }
7888
+ const key = fieldMatch[1];
7889
+ const inline = fieldMatch[2].trim();
7890
+ if (key === "files-read" || key === "artifacts") {
7891
+ const items = [];
7892
+ let j = i + 1;
7893
+ while (j < lines.length) {
7894
+ const sub = lines[j];
7895
+ if (topFieldRe.test(sub))
7896
+ break;
7897
+ if (/^#/.test(sub))
7898
+ break;
7899
+ const itemMatch = sub.match(/^\s{2,}-\s+(.+?)\s*$/);
7900
+ if (itemMatch) {
7901
+ items.push(itemMatch[1].trim());
7693
7902
  }
7694
- return this._interpreter;
7695
- }
7696
- async scan(absolutePath, repoRoot) {
7697
- const result = await this.scanBatch([absolutePath], repoRoot);
7698
- return result.entries[0] ?? null;
7903
+ j++;
7699
7904
  }
7700
- async scanBatch(absolutePaths, repoRoot) {
7701
- const interpreter = this.getInterpreter();
7702
- const entries = [];
7703
- const warnings = [];
7704
- if (!interpreter) {
7705
- const count = absolutePaths.length;
7706
- warnings.push({
7707
- path: "",
7708
- message: `python interpreter not found on PATH; skipping ${count} .py file${count === 1 ? "" : "s"}`
7709
- });
7710
- return { entries, warnings };
7711
- }
7712
- const scriptPath = ASSET.codeMapPython;
7713
- const rand = Math.random().toString(36).slice(2);
7714
- const listFile = join16(tmpdir(), `cdd-codemap-${process.pid}-${rand}.txt`);
7715
- writeFileSync6(listFile, absolutePaths.join("\n") + "\n", "utf8");
7716
- let stdout = "";
7717
- let stderr = "";
7718
- let exitCode = 0;
7719
- try {
7720
- const result = spawnSync2(
7721
- interpreter,
7722
- [scriptPath, "--batch-file", listFile, "--repo-root", repoRoot],
7723
- {
7724
- encoding: "utf8",
7725
- timeout: TIMEOUT_MS,
7726
- maxBuffer: 50 * 1024 * 1024
7727
- // 50MB
7728
- }
7729
- );
7730
- stdout = result.stdout ?? "";
7731
- stderr = result.stderr ?? "";
7732
- exitCode = result.status ?? -1;
7733
- if (result.error) {
7734
- const errMsg = result.error.message ?? String(result.error);
7735
- if (errMsg.includes("ENOENT")) {
7736
- warnings.push({
7737
- path: "",
7738
- message: `python interpreter not found (ENOENT); skipping ${absolutePaths.length} .py file(s)`
7739
- });
7740
- return { entries, warnings };
7741
- }
7742
- if (errMsg.includes("ETIMEDOUT") || errMsg.includes("timeout")) {
7743
- warnings.push({
7744
- path: "",
7745
- message: `python scanner timed out after ${TIMEOUT_MS}ms; skipping .py files`
7746
- });
7747
- return { entries, warnings };
7748
- }
7749
- warnings.push({
7750
- path: "",
7751
- message: `python scanner error: ${errMsg}; skipping .py files`
7752
- });
7753
- return { entries, warnings };
7754
- }
7755
- } finally {
7756
- try {
7757
- unlinkSync(listFile);
7758
- } catch {
7759
- }
7760
- }
7761
- if (exitCode === 3) {
7762
- warnings.push({
7763
- path: "",
7764
- message: "python interpreter is < 3.9 (need ast.unparse); skipping .py files"
7765
- });
7766
- return { entries, warnings };
7767
- }
7768
- for (const line of stdout.split("\n")) {
7769
- const trimmed = line.trim();
7770
- if (!trimmed)
7771
- continue;
7772
- let parsed;
7773
- try {
7774
- parsed = JSON.parse(trimmed);
7775
- } catch {
7776
- continue;
7777
- }
7778
- if (!parsed.ok) {
7779
- warnings.push({ path: parsed.path, message: parsed.error });
7780
- continue;
7905
+ if (key === "files-read") {
7906
+ data["files-read"] = items;
7907
+ } else {
7908
+ data["artifacts"] = items.map((s) => {
7909
+ const idx = s.indexOf(":");
7910
+ if (idx === -1) {
7911
+ return { type: "note", pointer: s };
7781
7912
  }
7782
- const r = parsed;
7783
- entries.push({
7784
- path: canonicalRelPath(join16(repoRoot, r.path), repoRoot),
7785
- total_lines: r.total_lines,
7786
- imports: r.imports ?? [],
7787
- constants: r.constants ?? [],
7788
- classes: (r.classes ?? []).map((c) => ({
7789
- name: c.name,
7790
- lines: [c.lines[0], c.lines[1]],
7791
- methods: (c.methods ?? []).map((m) => ({
7792
- name: m.name,
7793
- lines: [m.lines[0], m.lines[1]],
7794
- async: m.async
7795
- }))
7796
- })),
7797
- functions: (r.functions ?? []).map((f) => ({
7798
- name: f.name,
7799
- lines: [f.lines[0], f.lines[1]],
7800
- decorators: f.decorators ?? [],
7801
- async: f.async
7802
- }))
7803
- });
7804
- }
7805
- if (exitCode === 2) {
7806
- warnings.push({
7807
- path: "",
7808
- message: `python scanner exited with code 2 (fatal); partial results only. stderr: ${stderr.slice(0, 200)}`
7809
- });
7810
- }
7811
- return { entries, warnings };
7913
+ const type = s.slice(0, idx).trim();
7914
+ const pointer = s.slice(idx + 1).trim();
7915
+ return { type, pointer };
7916
+ });
7812
7917
  }
7813
- };
7814
- pythonScanner = new PythonScanner();
7815
- }
7816
- });
7817
-
7818
- // src/code-map/scanners/javascript.ts
7819
- var javascript_exports = {};
7820
- __export(javascript_exports, {
7821
- COMMON_PLUGINS: () => COMMON_PLUGINS,
7822
- countSourceLines: () => countSourceLines,
7823
- jsScanner: () => jsScanner,
7824
- parseAndExtract: () => parseAndExtract,
7825
- parseJsSource: () => parseJsSource,
7826
- parseSourceWithPlugins: () => parseSourceWithPlugins
7827
- });
7828
- import { readFileSync as readFileSync12 } from "fs";
7829
- import { parse } from "@babel/parser";
7830
- function parseSourceWithPlugins(source, plugins) {
7831
- return parse(source, {
7832
- sourceType: "unambiguous",
7833
- allowReturnOutsideFunction: true,
7834
- allowAwaitOutsideFunction: true,
7835
- errorRecovery: true,
7836
- plugins
7837
- });
7838
- }
7839
- function isCallExpression(node, callee) {
7840
- if (!node || node.type !== "CallExpression")
7841
- return false;
7842
- const c = node.callee;
7843
- return c.type === "Identifier" && c.name === callee;
7844
- }
7845
- function extractRequireModule(node) {
7846
- if (!node || node.type !== "CallExpression")
7847
- return null;
7848
- const c = node.callee;
7849
- if (c.type !== "Identifier" || c.name !== "require")
7850
- return null;
7851
- const arg = node.arguments[0];
7852
- if (!arg || arg.type !== "StringLiteral")
7853
- return null;
7854
- return arg.value;
7855
- }
7856
- function getLineRange(node) {
7857
- const start = node.loc?.start.line ?? 1;
7858
- const end = node.loc?.end.line ?? start;
7859
- return [start, end];
7860
- }
7861
- function processImportDeclaration(node) {
7862
- const items = [];
7863
- for (const spec of node.specifiers) {
7864
- if (spec.type === "ImportDefaultSpecifier") {
7865
- items.push(`default:${spec.local.name}`);
7866
- } else if (spec.type === "ImportNamespaceSpecifier") {
7867
- items.push(`*:${spec.local.name}`);
7868
- } else if (spec.type === "ImportSpecifier") {
7869
- const imported = spec.imported;
7870
- items.push(imported.type === "Identifier" ? imported.name : spec.local.name);
7918
+ i = j;
7919
+ continue;
7871
7920
  }
7921
+ data[key] = inline;
7922
+ i++;
7872
7923
  }
7873
- return {
7874
- module: node.source.value,
7875
- items,
7876
- line: node.loc?.start.line ?? 1
7877
- };
7924
+ return data;
7878
7925
  }
7879
- function processFunctionDeclaration(node, nameOverride) {
7880
- const name = nameOverride ?? node.id?.name;
7881
- if (!name)
7882
- return null;
7883
- return {
7884
- name,
7885
- lines: getLineRange(node),
7886
- decorators: [],
7887
- async: node.async
7888
- };
7926
+ function migrateAgentLogs(changeDir, changed, pendingWrites, pendingDeletes) {
7927
+ const agentLogDir = join16(changeDir, "agent-log");
7928
+ if (!existsSync14(agentLogDir))
7929
+ return;
7930
+ const mdLogs = readdirSync8(agentLogDir).filter((f) => f.endsWith(".md"));
7931
+ for (const f of mdLogs) {
7932
+ const fullPath = join16(agentLogDir, f);
7933
+ const yamlName = f.replace(/\.md$/, ".yml");
7934
+ const yamlFull = join16(agentLogDir, yamlName);
7935
+ if (existsSync14(yamlFull))
7936
+ continue;
7937
+ const raw = readFileSync11(fullPath, "utf8");
7938
+ const parsed = parseLegacyAgentLog(raw);
7939
+ const yamlOut = yaml3.dump(parsed, { lineWidth: -1, noRefs: true });
7940
+ pendingWrites.push({ path: yamlFull, content: yamlOut });
7941
+ pendingDeletes.push({ path: fullPath });
7942
+ changed.push(`agent-log/${f} -> agent-log/${yamlName}`);
7943
+ }
7889
7944
  }
7890
- function processClassDeclaration(node, nameOverride) {
7891
- const name = nameOverride ?? node.id?.name;
7892
- if (!name)
7893
- return null;
7894
- const methods = [];
7895
- for (const member of node.body.body) {
7896
- if (member.type === "ClassMethod" || member.type === "ClassPrivateMethod") {
7897
- const m = member;
7898
- let methodName;
7899
- if (m.key.type === "Identifier") {
7900
- methodName = m.key.name;
7901
- } else if (m.key.type === "PrivateName") {
7902
- methodName = `#${m.key.id.name}`;
7945
+ function migrateOne(changeId, changeDir, enableContextGovernance) {
7946
+ const changed = [];
7947
+ const warnings = [];
7948
+ const pending = [];
7949
+ const deletes = [];
7950
+ let detectedTier = null;
7951
+ const classifPath = join16(changeDir, "change-classification.md");
7952
+ if (existsSync14(classifPath)) {
7953
+ const content = readFileSync11(classifPath, "utf8");
7954
+ const hasNewTierFormat = /^## Tier\s*\n\s*-\s*\d\s*$/m.test(content);
7955
+ const oldMatch = content.match(/\*\*Tier[:\*]+\s*(?:Tier\s*)?(\d)/i) ?? content.match(/^-?\s*Tier:\s*(?:Tier\s*)?(\d)/mi);
7956
+ if (oldMatch)
7957
+ detectedTier = oldMatch[1];
7958
+ if (!hasNewTierFormat) {
7959
+ if (detectedTier) {
7960
+ const addition = `
7961
+ ## Tier
7962
+ - ${detectedTier}
7963
+ `;
7964
+ if (!content.includes("\n## Tier\n")) {
7965
+ changed.push(
7966
+ `change-classification.md: appended "## Tier\\n- ${detectedTier}" (converted from old format)`
7967
+ );
7968
+ pending.push({ path: classifPath, content: content + addition });
7969
+ }
7903
7970
  } else {
7904
- methodName = "<computed>";
7971
+ warnings.push(
7972
+ "change-classification.md: could not detect tier (no **Tier:** N or ## Tier N found). Set `tier: <0-5>` in tasks.yml frontmatter to enable tier-based gate checks."
7973
+ );
7905
7974
  }
7906
- methods.push({
7907
- name: methodName,
7908
- lines: getLineRange(m),
7909
- async: m.async
7910
- });
7975
+ } else {
7976
+ const structured = content.match(/^## Tier\s*\n\s*-\s*(\d)\s*$/m);
7977
+ if (structured)
7978
+ detectedTier = structured[1];
7911
7979
  }
7912
7980
  }
7913
- return {
7914
- name,
7915
- lines: getLineRange(node),
7916
- methods
7917
- };
7981
+ migrateTasksFile(changeId, changeDir, enableContextGovernance, detectedTier, changed, warnings, pending, deletes);
7982
+ migrateAgentLogs(changeDir, changed, pending, deletes);
7983
+ const manifestPath = join16(changeDir, "context-manifest.md");
7984
+ if (!existsSync14(manifestPath)) {
7985
+ changed.push(enableContextGovernance ? "context-manifest.md: added context-governance v1 manifest scaffold" : "context-manifest.md: added legacy context manifest scaffold");
7986
+ pending.push({
7987
+ path: manifestPath,
7988
+ content: enableContextGovernance ? buildContextGovernedManifest(changeId) : buildLegacyContextManifest(changeId)
7989
+ });
7990
+ } else if (enableContextGovernance) {
7991
+ warnings.push("context-manifest.md already exists \u2014 review allowed paths before relying on context-governance: v1");
7992
+ }
7993
+ return { result: { changed, warnings }, pending, deletes };
7918
7994
  }
7919
- function processVariableDeclaration(node, imports, constants, functions) {
7920
- for (const decl of node.declarations) {
7921
- if (!decl.id || decl.id.type !== "Identifier")
7922
- continue;
7923
- const varName = decl.id.name;
7924
- const init2 = decl.init;
7925
- if (init2 && isCallExpression(init2, "require")) {
7926
- const mod = extractRequireModule(init2);
7927
- if (mod) {
7928
- imports.push({ module: mod, items: [`default:${varName}`], line: node.loc?.start.line ?? 1 });
7995
+ function commitWritesAtomically(pending, deletes) {
7996
+ const renames = [];
7997
+ try {
7998
+ for (const write of pending) {
7999
+ const tmp = `${write.path}.cdd-migrate.tmp`;
8000
+ writeFileSync6(tmp, write.content, "utf8");
8001
+ renames.push({ tmp, final: write.path });
8002
+ }
8003
+ } catch (err) {
8004
+ for (const r of renames) {
8005
+ try {
8006
+ rmSync2(r.tmp, { force: true });
8007
+ } catch {
7929
8008
  }
8009
+ }
8010
+ throw err;
8011
+ }
8012
+ for (const r of renames) {
8013
+ renameSync(r.tmp, r.final);
8014
+ }
8015
+ for (const d of deletes) {
8016
+ try {
8017
+ rmSync2(d.path, { force: true });
8018
+ } catch {
8019
+ }
8020
+ }
8021
+ }
8022
+ async function migrate(changeId, opts = {}) {
8023
+ const cwd = process.cwd();
8024
+ const dryRun = opts.dryRun ?? false;
8025
+ const enableContextGovernance = opts.enableContextGovernance ?? false;
8026
+ const noBackup = opts.noBackup ?? false;
8027
+ const idsToMigrate = [];
8028
+ if (opts.all) {
8029
+ const changesDir = join16(cwd, "specs", "changes");
8030
+ if (!existsSync14(changesDir)) {
8031
+ log.info("No specs/changes/ directory found \u2014 nothing to migrate.");
8032
+ return;
8033
+ }
8034
+ idsToMigrate.push(
8035
+ ...readdirSync8(changesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name)
8036
+ );
8037
+ } else if (changeId) {
8038
+ const specificDir = join16(cwd, "specs", "changes", changeId);
8039
+ if (!existsSync14(specificDir)) {
8040
+ log.error(`Change not found: specs/changes/${changeId}`);
8041
+ process.exit(1);
8042
+ }
8043
+ idsToMigrate.push(changeId);
8044
+ } else {
8045
+ log.error("Usage: cdd-kit migrate <change-id> | cdd-kit migrate --all [--dry-run] [--no-backup]");
8046
+ process.exit(1);
8047
+ }
8048
+ if (idsToMigrate.length === 0) {
8049
+ log.info("No changes found to migrate.");
8050
+ return;
8051
+ }
8052
+ if (dryRun) {
8053
+ log.info("Dry run \u2014 no files will be written.");
8054
+ log.blank();
8055
+ }
8056
+ const sessionStamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
8057
+ let migratedCount = 0;
8058
+ let upToDateCount = 0;
8059
+ const backupRoot = join16(cwd, ".cdd", "migrate-backup", sessionStamp);
8060
+ for (const id of idsToMigrate) {
8061
+ const changeDir = join16(cwd, "specs", "changes", id);
8062
+ if (!existsSync14(changeDir)) {
8063
+ log.warn(` ${id}: directory not found \u2014 skipping`);
7930
8064
  continue;
7931
8065
  }
7932
- if (isAllCapsConst(varName) && init2 !== null && init2 !== void 0) {
7933
- constants.push({ name: varName, line: node.loc?.start.line ?? 1 });
8066
+ const { result, pending, deletes } = migrateOne(id, changeDir, enableContextGovernance);
8067
+ const { changed, warnings } = result;
8068
+ if (changed.length === 0) {
8069
+ log.info(` ${id}: already up to date`);
8070
+ upToDateCount++;
8071
+ for (const w of warnings)
8072
+ log.warn(` ${id}: ${w}`);
7934
8073
  continue;
7935
8074
  }
7936
- if (init2 && (init2.type === "ArrowFunctionExpression" || init2.type === "FunctionExpression")) {
7937
- functions.push({
7938
- name: varName,
7939
- lines: getLineRange(node),
7940
- decorators: [],
7941
- async: init2.async
7942
- });
7943
- } else if (init2 && init2.type === "CallExpression" && /^[A-Z]/.test(varName)) {
7944
- functions.push({
7945
- name: varName,
7946
- lines: getLineRange(node),
7947
- decorators: [],
7948
- async: false
7949
- });
8075
+ if (!dryRun) {
8076
+ try {
8077
+ if (!noBackup)
8078
+ backupChangeDir(cwd, id, sessionStamp);
8079
+ commitWritesAtomically(pending, deletes);
8080
+ } catch (err) {
8081
+ log.error(` ${id}: migration failed \u2014 ${err.message}`);
8082
+ if (!noBackup) {
8083
+ log.error(` ${id}: restore from .cdd/migrate-backup/${sessionStamp}/${id}/`);
8084
+ }
8085
+ process.exit(1);
8086
+ }
8087
+ }
8088
+ log.ok(` ${id}: migrated`);
8089
+ for (const c of changed)
8090
+ log.info(` + ${c}`);
8091
+ migratedCount++;
8092
+ for (const w of warnings)
8093
+ log.warn(` ${id}: ${w}`);
8094
+ }
8095
+ log.blank();
8096
+ if (dryRun) {
8097
+ log.info(`Dry run complete: ${migratedCount} change(s) would be updated, ${upToDateCount} already up to date.`);
8098
+ } else {
8099
+ log.ok(`Migration complete: ${migratedCount} updated, ${upToDateCount} already up to date.`);
8100
+ if (migratedCount > 0 && !noBackup) {
8101
+ log.info(`Backup: ${backupRoot}`);
8102
+ if (ensureGitignoreEntry(cwd, ".cdd/migrate-backup/")) {
8103
+ log.info("Added `.cdd/migrate-backup/` to .gitignore");
8104
+ }
8105
+ log.info('Next: git add specs/changes/ && git commit -m "chore: migrate changes to YAML format"');
8106
+ log.info("When stable, remove backup: rm -rf .cdd/migrate-backup/");
7950
8107
  }
7951
8108
  }
7952
8109
  }
7953
- function processTsInterfaceDeclaration(node, exported) {
7954
- if (!node || node.type !== "TSInterfaceDeclaration")
7955
- return null;
7956
- if (!node.id || node.id.type !== "Identifier")
7957
- return null;
7958
- return {
7959
- name: node.id.name,
7960
- lines: getLineRange(node),
7961
- exported
7962
- };
7963
- }
7964
- function processTsTypeAliasDeclaration(node, exported) {
7965
- if (!node || node.type !== "TSTypeAliasDeclaration")
7966
- return null;
7967
- if (!node.id || node.id.type !== "Identifier")
7968
- return null;
7969
- return {
7970
- name: node.id.name,
7971
- lines: getLineRange(node),
7972
- exported
7973
- };
8110
+ function ensureGitignoreEntry(cwd, entry) {
8111
+ const path = join16(cwd, ".gitignore");
8112
+ const trimmed = entry.trim();
8113
+ if (!trimmed)
8114
+ return false;
8115
+ const re = new RegExp(`^\\s*${trimmed.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\$&")}\\s*$`, "m");
8116
+ let existing = "";
8117
+ if (existsSync14(path))
8118
+ existing = readFileSync11(path, "utf8");
8119
+ if (re.test(existing))
8120
+ return false;
8121
+ const sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
8122
+ const block = existing.length === 0 ? `# cdd-kit generated backups (do not commit)
8123
+ ${trimmed}
8124
+ ` : `${sep}
8125
+ # cdd-kit generated backups (do not commit)
8126
+ ${trimmed}
8127
+ `;
8128
+ writeFileSync6(path, existing + block, "utf8");
8129
+ return true;
7974
8130
  }
7975
- function processTsEnumDeclaration(node, exported) {
7976
- if (!node || node.type !== "TSEnumDeclaration")
7977
- return null;
7978
- if (!node.id || node.id.type !== "Identifier")
7979
- return null;
7980
- const members = [];
7981
- for (const m of node.members ?? []) {
7982
- if (!m || !m.id)
8131
+ var init_migrate = __esm({
8132
+ "src/commands/migrate.ts"() {
8133
+ "use strict";
8134
+ init_logger();
8135
+ }
8136
+ });
8137
+
8138
+ // src/commands/upgrade.ts
8139
+ var upgrade_exports = {};
8140
+ __export(upgrade_exports, {
8141
+ upgrade: () => upgrade
8142
+ });
8143
+ import { existsSync as existsSync15, mkdirSync as mkdirSync7, readdirSync as readdirSync9, copyFileSync as copyFileSync3, readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "fs";
8144
+ import { dirname as dirname4, join as join17, relative as relative3 } from "path";
8145
+ function planMissingFiles(srcDir, destDir, label, planned) {
8146
+ if (!existsSync15(srcDir))
8147
+ return;
8148
+ for (const entry of readdirSync9(srcDir, { withFileTypes: true })) {
8149
+ const src = join17(srcDir, entry.name);
8150
+ const dest = join17(destDir, entry.name);
8151
+ if (entry.isDirectory()) {
8152
+ planMissingFiles(src, dest, join17(label, entry.name), planned);
7983
8153
  continue;
7984
- if (m.id.type === "Identifier")
7985
- members.push(m.id.name);
7986
- else if (m.id.type === "StringLiteral")
7987
- members.push(m.id.value);
8154
+ }
8155
+ if (!existsSync15(dest)) {
8156
+ planned.push({ src, dest, rel: join17(label, relative3(srcDir, src)) });
8157
+ }
7988
8158
  }
7989
- return {
7990
- name: node.id.name,
7991
- lines: getLineRange(node),
7992
- exported,
7993
- members
7994
- };
7995
8159
  }
7996
- function processStatement(stmt, buckets, extractTsTypes, exportedFromWrapper = false) {
7997
- if (stmt.type === "ExportNamedDeclaration" && stmt.declaration) {
7998
- processStatement(stmt.declaration, buckets, extractTsTypes, true);
7999
- return;
8160
+ function planProviderGuidance(cwd, provider, planned) {
8161
+ if (provider === "claude" || provider === "both") {
8162
+ if (!existsSync15(join17(cwd, "CLAUDE.md"))) {
8163
+ planned.push({ src: ASSET.claudeTemplate, dest: join17(cwd, "CLAUDE.md"), rel: "CLAUDE.md" });
8164
+ }
8165
+ if (!existsSync15(join17(cwd, "AGENTS.md"))) {
8166
+ planned.push({ src: ASSET.agentsTemplate, dest: join17(cwd, "AGENTS.md"), rel: "AGENTS.md" });
8167
+ }
8000
8168
  }
8001
- if (stmt.type === "ExportDefaultDeclaration") {
8002
- const decl = stmt.declaration;
8003
- if (decl.type === "FunctionDeclaration") {
8004
- const fe = processFunctionDeclaration(decl, decl.id?.name ?? "default");
8005
- if (fe)
8006
- buckets.functions.push(fe);
8007
- } else if (decl.type === "ClassDeclaration") {
8008
- const ce = processClassDeclaration(decl, decl.id?.name ?? "default");
8009
- if (ce)
8010
- buckets.classes.push(ce);
8169
+ if ((provider === "codex" || provider === "both") && !existsSync15(join17(cwd, "CODEX.md"))) {
8170
+ planned.push({ src: ASSET.codexTemplate, dest: join17(cwd, "CODEX.md"), rel: "CODEX.md" });
8171
+ }
8172
+ }
8173
+ function applyCopy(plan) {
8174
+ for (const item of plan) {
8175
+ mkdirSync7(dirname4(item.dest), { recursive: true });
8176
+ copyFileSync3(item.src, item.dest);
8177
+ }
8178
+ }
8179
+ async function upgrade(opts = {}) {
8180
+ const cwd = process.cwd();
8181
+ const requestedProvider = opts.provider ?? "auto";
8182
+ if (!validateProviderOption(requestedProvider)) {
8183
+ log.error(`Invalid provider: ${requestedProvider}. Use auto, claude, codex, or both.`);
8184
+ process.exit(1);
8185
+ }
8186
+ const provider = inferProvider(cwd, requestedProvider);
8187
+ const plan = [];
8188
+ planMissingFiles(ASSET.contracts, join17(cwd, "contracts"), "contracts", plan);
8189
+ planMissingFiles(ASSET.specsTemplates, join17(cwd, "specs", "templates"), "specs/templates", plan);
8190
+ planMissingFiles(ASSET.testsTemplates, join17(cwd, "tests", "templates"), "tests/templates", plan);
8191
+ planMissingFiles(ASSET.ci, join17(cwd, "ci"), "ci", plan);
8192
+ planMissingFiles(ASSET.githubWorkflows, join17(cwd, ".github", "workflows"), ".github/workflows", plan);
8193
+ planMissingFiles(ASSET.cddConfig, join17(cwd, ".cdd"), ".cdd", plan);
8194
+ planProviderGuidance(cwd, provider, plan);
8195
+ log.blank();
8196
+ log.info(`Upgrade provider: ${provider}`);
8197
+ if (plan.length === 0) {
8198
+ log.ok("No missing cdd-kit project files found.");
8199
+ if (opts.migrateChanges) {
8200
+ log.blank();
8201
+ log.info("Running change migration flow...");
8202
+ await migrate(void 0, {
8203
+ all: true,
8204
+ dryRun: !opts.yes,
8205
+ enableContextGovernance: opts.enableContextGovernance
8206
+ });
8011
8207
  }
8208
+ log.blank();
8012
8209
  return;
8013
8210
  }
8014
- if (stmt.type === "ImportDeclaration") {
8015
- buckets.imports.push(processImportDeclaration(stmt));
8211
+ log.info(`${plan.length} missing file(s) detected:`);
8212
+ for (const item of plan)
8213
+ log.dim(` + ${item.rel.replace(/\\/g, "/")}`);
8214
+ if (!opts.yes) {
8215
+ log.blank();
8216
+ log.info("Dry run only. Re-run with --yes to write missing files.");
8217
+ if (opts.migrateChanges) {
8218
+ log.blank();
8219
+ log.info("Previewing existing change migration because --migrate-changes was requested.");
8220
+ await migrate(void 0, {
8221
+ all: true,
8222
+ dryRun: true,
8223
+ enableContextGovernance: opts.enableContextGovernance
8224
+ });
8225
+ }
8226
+ log.blank();
8016
8227
  return;
8017
8228
  }
8018
- if (stmt.type === "FunctionDeclaration") {
8019
- const fe = processFunctionDeclaration(stmt);
8020
- if (fe)
8021
- buckets.functions.push(fe);
8022
- return;
8229
+ applyCopy(plan);
8230
+ const modelPolicyPath = join17(cwd, ".cdd", "model-policy.json");
8231
+ if (existsSync15(modelPolicyPath)) {
8232
+ let existing = {};
8233
+ try {
8234
+ existing = JSON.parse(readFileSync12(modelPolicyPath, "utf8"));
8235
+ } catch {
8236
+ }
8237
+ const merged = {
8238
+ ...existing,
8239
+ provider,
8240
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
8241
+ roles: existing.roles && typeof existing.roles === "object" ? existing.roles : {}
8242
+ };
8243
+ writeFileSync7(modelPolicyPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
8023
8244
  }
8024
- if (stmt.type === "ClassDeclaration") {
8025
- const ce = processClassDeclaration(stmt);
8026
- if (ce)
8027
- buckets.classes.push(ce);
8028
- return;
8245
+ log.blank();
8246
+ log.ok(`Upgrade complete: ${plan.length} missing file(s) added.`);
8247
+ log.info("Existing project guidance and contracts were preserved.");
8248
+ if (opts.migrateChanges) {
8249
+ log.blank();
8250
+ log.info("Running change migration flow...");
8251
+ await migrate(void 0, {
8252
+ all: true,
8253
+ dryRun: false,
8254
+ enableContextGovernance: opts.enableContextGovernance
8255
+ });
8029
8256
  }
8030
- if (stmt.type === "VariableDeclaration") {
8031
- processVariableDeclaration(stmt, buckets.imports, buckets.constants, buckets.functions);
8032
- return;
8257
+ log.blank();
8258
+ }
8259
+ var init_upgrade = __esm({
8260
+ "src/commands/upgrade.ts"() {
8261
+ "use strict";
8262
+ init_paths();
8263
+ init_logger();
8264
+ init_provider();
8265
+ init_migrate();
8033
8266
  }
8034
- if (extractTsTypes) {
8035
- const anyStmt = stmt;
8036
- if (anyStmt.type === "TSInterfaceDeclaration") {
8037
- const e = processTsInterfaceDeclaration(anyStmt, exportedFromWrapper);
8038
- if (e)
8039
- buckets.interfaces.push(e);
8040
- return;
8267
+ });
8268
+
8269
+ // src/code-map/yaml-writer.ts
8270
+ function quoteScalar(s) {
8271
+ if (s.startsWith(".") || YAML_RESERVED.test(s) || s.includes(" ") || s === "" || s === "null" || s === "true" || s === "false") {
8272
+ return `'${s.replace(/'/g, "''")}'`;
8273
+ }
8274
+ return s;
8275
+ }
8276
+ function quotePath(p) {
8277
+ if (YAML_RESERVED.test(p) || p.includes(" ")) {
8278
+ return `'${p.replace(/'/g, "''")}'`;
8279
+ }
8280
+ return p;
8281
+ }
8282
+ function renderItems(items) {
8283
+ if (items.length === 0)
8284
+ return "[]";
8285
+ return `[${items.map(quoteScalar).join(", ")}]`;
8286
+ }
8287
+ function truncateDecorator(d, max = 80) {
8288
+ const cleaned = d.replace(/\r?\n/g, " ");
8289
+ return cleaned.length <= max ? cleaned : cleaned.slice(0, max) + "...";
8290
+ }
8291
+ function renderClass(c) {
8292
+ const lines = [];
8293
+ lines.push(` - name: ${c.name}`);
8294
+ lines.push(` lines: ${c.lines[0]}-${c.lines[1]}`);
8295
+ if (c.methods.length > 0) {
8296
+ lines.push(" methods:");
8297
+ for (const m of c.methods) {
8298
+ const asyncPrefix = m.async ? "async " : "";
8299
+ lines.push(` - { name: ${asyncPrefix}${m.name}, lines: ${m.lines[0]}-${m.lines[1]} }`);
8041
8300
  }
8042
- if (anyStmt.type === "TSTypeAliasDeclaration") {
8043
- const e = processTsTypeAliasDeclaration(anyStmt, exportedFromWrapper);
8044
- if (e)
8045
- buckets.types.push(e);
8046
- return;
8301
+ }
8302
+ return lines;
8303
+ }
8304
+ function renderFunction(f) {
8305
+ const asyncPrefix = f.async ? "async " : "";
8306
+ let comment = "";
8307
+ if (f.decorators.length > 0) {
8308
+ const truncated = truncateDecorator(f.decorators[0]);
8309
+ comment = ` # @${truncated}`;
8310
+ }
8311
+ return ` - { name: ${asyncPrefix}${f.name}, lines: ${f.lines[0]}-${f.lines[1]} }${comment}`;
8312
+ }
8313
+ function renderTypeDef(t) {
8314
+ const exportedSuffix = t.exported ? "" : " # local";
8315
+ return ` - { name: ${t.name}, lines: ${t.lines[0]}-${t.lines[1]} }${exportedSuffix}`;
8316
+ }
8317
+ function renderEnum(e) {
8318
+ const lines = [];
8319
+ const exportedSuffix = e.exported ? "" : " # local";
8320
+ lines.push(` - name: ${e.name}`);
8321
+ lines.push(` lines: ${e.lines[0]}-${e.lines[1]}${exportedSuffix}`);
8322
+ if (e.members.length > 0) {
8323
+ lines.push(` members: [${e.members.join(", ")}]`);
8324
+ }
8325
+ return lines;
8326
+ }
8327
+ function renderYaml(entries, opts) {
8328
+ const now = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "Z");
8329
+ const totalSrc = entries.reduce((s, e) => s + e.total_lines, 0);
8330
+ const bodyLines = [];
8331
+ for (let i = 0; i < entries.length; i++) {
8332
+ const e = entries[i];
8333
+ if (i > 0)
8334
+ bodyLines.push("");
8335
+ const pathKey = quotePath(e.path);
8336
+ bodyLines.push(`${pathKey}: # ${e.total_lines} lines`);
8337
+ if (e.imports.length > 0) {
8338
+ bodyLines.push(" imports:");
8339
+ for (const imp of e.imports) {
8340
+ const mod = quoteScalar(imp.module);
8341
+ bodyLines.push(` - { module: ${mod}, items: ${renderItems(imp.items)}, line: ${imp.line} }`);
8342
+ }
8047
8343
  }
8048
- if (anyStmt.type === "TSEnumDeclaration") {
8049
- const e = processTsEnumDeclaration(anyStmt, exportedFromWrapper);
8050
- if (e)
8051
- buckets.enums.push(e);
8052
- return;
8344
+ if (e.constants.length > 0) {
8345
+ bodyLines.push(" constants:");
8346
+ for (const c of e.constants) {
8347
+ bodyLines.push(` - { name: ${c.name}, line: ${c.line} }`);
8348
+ }
8349
+ }
8350
+ if (e.classes.length > 0) {
8351
+ bodyLines.push(" classes:");
8352
+ for (const c of e.classes) {
8353
+ bodyLines.push(...renderClass(c));
8354
+ }
8355
+ }
8356
+ if (e.functions.length > 0) {
8357
+ bodyLines.push(" functions:");
8358
+ for (const f of e.functions) {
8359
+ bodyLines.push(renderFunction(f));
8360
+ }
8361
+ }
8362
+ if (e.interfaces && e.interfaces.length > 0) {
8363
+ bodyLines.push(" interfaces:");
8364
+ for (const t of e.interfaces) {
8365
+ bodyLines.push(renderTypeDef(t));
8366
+ }
8367
+ }
8368
+ if (e.types && e.types.length > 0) {
8369
+ bodyLines.push(" types:");
8370
+ for (const t of e.types) {
8371
+ bodyLines.push(renderTypeDef(t));
8372
+ }
8373
+ }
8374
+ if (e.enums && e.enums.length > 0) {
8375
+ bodyLines.push(" enums:");
8376
+ for (const en of e.enums) {
8377
+ bodyLines.push(...renderEnum(en));
8378
+ }
8053
8379
  }
8054
8380
  }
8055
- if (stmt.type === "ExpressionStatement") {
8056
- const expr = stmt.expression;
8057
- if (isCallExpression(expr, "require")) {
8058
- const mod = extractRequireModule(expr);
8059
- if (mod) {
8060
- buckets.imports.push({ module: mod, items: [], line: stmt.loc?.start.line ?? 1 });
8381
+ const mapLines = bodyLines.length + 2;
8382
+ const compression = totalSrc === 0 ? 0 : totalSrc / mapLines;
8383
+ const fileCount = entries.length;
8384
+ const header = [
8385
+ `# generated: ${now} by ${opts.generator}`,
8386
+ `# files: ${fileCount}, src-lines: ${totalSrc}, map-lines: ${mapLines}, compression: ${compression.toFixed(1)}x`
8387
+ ];
8388
+ return [...header, ...bodyLines].join("\n") + "\n";
8389
+ }
8390
+ var YAML_RESERVED;
8391
+ var init_yaml_writer = __esm({
8392
+ "src/code-map/yaml-writer.ts"() {
8393
+ "use strict";
8394
+ YAML_RESERVED = /[:[\]{},&*!|>'"%@`#]/;
8395
+ }
8396
+ });
8397
+
8398
+ // src/code-map/orchestrator.ts
8399
+ async function scanInProcess(scanner, absolutePaths, repoRoot) {
8400
+ const entries = [];
8401
+ const warnings = [];
8402
+ for (const absPath of absolutePaths) {
8403
+ try {
8404
+ const entry = await scanner.scan(absPath, repoRoot);
8405
+ if (entry === null) {
8406
+ const rel = absPath.replace(/\\/g, "/").replace(repoRoot.replace(/\\/g, "/") + "/", "");
8407
+ warnings.push({ path: rel, message: "parse error (scanner returned null)" });
8408
+ } else {
8409
+ entries.push(entry);
8061
8410
  }
8411
+ } catch (err) {
8412
+ const rel = absPath.replace(/\\/g, "/").replace(repoRoot.replace(/\\/g, "/") + "/", "");
8413
+ warnings.push({ path: rel, message: `IO error: ${err.message}` });
8062
8414
  }
8063
8415
  }
8416
+ return { entries, warnings };
8064
8417
  }
8065
- function parseAndExtract(source, opts) {
8066
- let ast;
8067
- try {
8068
- ast = parseSourceWithPlugins(source, opts.plugins);
8069
- } catch {
8070
- return null;
8418
+ function bucketByExtension(files) {
8419
+ const out = {};
8420
+ for (const f of files) {
8421
+ const dot = f.lastIndexOf(".");
8422
+ const ext = dot >= 0 ? f.slice(dot).toLowerCase() : "";
8423
+ if (!out[ext])
8424
+ out[ext] = [];
8425
+ out[ext].push(f);
8071
8426
  }
8072
- const buckets = {
8073
- imports: [],
8074
- constants: [],
8075
- functions: [],
8076
- classes: [],
8077
- interfaces: [],
8078
- types: [],
8079
- enums: []
8080
- };
8081
- for (const stmt of ast.program.body) {
8082
- processStatement(stmt, buckets, !!opts.extractTsTypes);
8427
+ return out;
8428
+ }
8429
+ var init_orchestrator = __esm({
8430
+ "src/code-map/orchestrator.ts"() {
8431
+ "use strict";
8432
+ init_include_exclude();
8083
8433
  }
8084
- return buckets;
8434
+ });
8435
+
8436
+ // src/code-map/scanners/common.ts
8437
+ import { relative as relative4 } from "path";
8438
+ function canonicalRelPath(absolutePath, repoRoot) {
8439
+ const rel = relative4(repoRoot, absolutePath);
8440
+ return rel.replace(/\\/g, "/").normalize("NFC");
8085
8441
  }
8086
- function countSourceLines(source) {
8087
- if (source === "")
8088
- return 0;
8089
- return source.split(/\r?\n/).length - (source.endsWith("\n") || source.endsWith("\r\n") ? 1 : 0);
8442
+ function isAllCapsConst(name) {
8443
+ return name.length >= 2 && /^[A-Z][A-Z0-9_]*$/.test(name);
8090
8444
  }
8091
- function parseJsSource(source, relPath) {
8092
- const total_lines = countSourceLines(source);
8093
- const r = parseAndExtract(source, { plugins: JS_PLUGINS, extractTsTypes: false });
8094
- if (!r)
8095
- return null;
8096
- return {
8097
- path: relPath,
8098
- total_lines,
8099
- imports: r.imports,
8100
- constants: r.constants,
8101
- classes: r.classes,
8102
- functions: r.functions
8103
- };
8445
+ function isBinary(content) {
8446
+ const head = content.slice(0, 4096);
8447
+ return head.includes("\0");
8104
8448
  }
8105
- var COMMON_PLUGINS, JS_PLUGINS, JavaScriptScanner, jsScanner;
8106
- var init_javascript = __esm({
8107
- "src/code-map/scanners/javascript.ts"() {
8449
+ var init_common = __esm({
8450
+ "src/code-map/scanners/common.ts"() {
8108
8451
  "use strict";
8109
- init_common();
8110
- COMMON_PLUGINS = [
8111
- "classProperties",
8112
- "classPrivateProperties",
8113
- "classPrivateMethods",
8114
- "decorators-legacy",
8115
- "topLevelAwait",
8116
- "dynamicImport",
8117
- "optionalChaining",
8118
- "nullishCoalescingOperator",
8119
- "objectRestSpread",
8120
- "asyncGenerators",
8121
- "numericSeparator",
8122
- "logicalAssignment"
8123
- ];
8124
- JS_PLUGINS = ["jsx", ...COMMON_PLUGINS];
8125
- JavaScriptScanner = class {
8126
- extensions = [".js"];
8127
- async scan(absolutePath, repoRoot) {
8128
- let source;
8129
- try {
8130
- source = readFileSync12(absolutePath, "utf8");
8131
- } catch (err) {
8132
- throw err;
8133
- }
8134
- if (isBinary(source)) {
8135
- return null;
8136
- }
8137
- const relPath = canonicalRelPath(absolutePath, repoRoot);
8138
- return parseJsSource(source, relPath);
8139
- }
8140
- };
8141
- jsScanner = new JavaScriptScanner();
8142
8452
  }
8143
8453
  });
8144
8454
 
8145
- // src/code-map/scanners/typescript.ts
8146
- var typescript_exports = {};
8147
- __export(typescript_exports, {
8148
- tsScanner: () => tsScanner
8455
+ // src/code-map/scanners/python.ts
8456
+ var python_exports = {};
8457
+ __export(python_exports, {
8458
+ pythonScanner: () => pythonScanner
8149
8459
  });
8150
- import { readFileSync as readFileSync13 } from "fs";
8151
- var TypeScriptScanner, tsScanner;
8152
- var init_typescript = __esm({
8153
- "src/code-map/scanners/typescript.ts"() {
8460
+ import { spawnSync as spawnSync2 } from "child_process";
8461
+ import { writeFileSync as writeFileSync8, unlinkSync } from "fs";
8462
+ import { join as join18 } from "path";
8463
+ import { tmpdir } from "os";
8464
+ function detectPython2() {
8465
+ for (const candidate of ["python3", "python"]) {
8466
+ try {
8467
+ const result = spawnSync2(candidate, ["--version"], {
8468
+ encoding: "utf8",
8469
+ timeout: 5e3
8470
+ });
8471
+ if (result.status === 0)
8472
+ return candidate;
8473
+ } catch {
8474
+ }
8475
+ }
8476
+ return null;
8477
+ }
8478
+ var TIMEOUT_MS, PythonScanner, pythonScanner;
8479
+ var init_python = __esm({
8480
+ "src/code-map/scanners/python.ts"() {
8154
8481
  "use strict";
8482
+ init_paths();
8155
8483
  init_common();
8156
- init_javascript();
8157
- TypeScriptScanner = class {
8158
- extensions = [".ts", ".tsx"];
8159
- async scan(absolutePath, repoRoot) {
8160
- let source;
8161
- try {
8162
- source = readFileSync13(absolutePath, "utf8");
8163
- } catch (err) {
8164
- throw err;
8165
- }
8166
- if (isBinary(source)) {
8167
- return null;
8484
+ TIMEOUT_MS = parseInt(process.env["CDD_CODE_MAP_TIMEOUT_MS"] ?? "30000", 10);
8485
+ PythonScanner = class {
8486
+ extensions = [".py"];
8487
+ _interpreter = void 0;
8488
+ getInterpreter() {
8489
+ if (this._interpreter === void 0) {
8490
+ this._interpreter = detectPython2();
8168
8491
  }
8169
- const isTsx = absolutePath.toLowerCase().endsWith(".tsx");
8170
- const plugins = isTsx ? ["typescript", "jsx", ...COMMON_PLUGINS] : ["typescript", ...COMMON_PLUGINS];
8171
- const r = parseAndExtract(source, { plugins, extractTsTypes: true });
8172
- if (!r)
8173
- return null;
8174
- const relPath = canonicalRelPath(absolutePath, repoRoot);
8175
- const total_lines = countSourceLines(source);
8176
- return {
8177
- path: relPath,
8178
- total_lines,
8179
- imports: r.imports,
8180
- constants: r.constants,
8181
- classes: r.classes,
8182
- functions: r.functions,
8183
- interfaces: r.interfaces,
8184
- types: r.types,
8185
- enums: r.enums
8186
- };
8492
+ return this._interpreter;
8187
8493
  }
8188
- };
8189
- tsScanner = new TypeScriptScanner();
8190
- }
8191
- });
8192
-
8193
- // src/code-map/scanners/vue.ts
8194
- var vue_exports = {};
8195
- __export(vue_exports, {
8196
- vueScanner: () => vueScanner
8197
- });
8198
- import { readFileSync as readFileSync14 } from "fs";
8199
- import { parse as parse2 } from "@vue/compiler-sfc";
8200
- var VueScanner, vueScanner;
8201
- var init_vue = __esm({
8202
- "src/code-map/scanners/vue.ts"() {
8203
- "use strict";
8204
- init_common();
8205
- init_javascript();
8206
- VueScanner = class {
8207
- extensions = [".vue"];
8208
8494
  async scan(absolutePath, repoRoot) {
8209
- let source;
8210
- try {
8211
- source = readFileSync14(absolutePath, "utf8");
8212
- } catch (err) {
8213
- throw err;
8214
- }
8215
- if (isBinary(source)) {
8216
- return null;
8217
- }
8218
- const relPath = canonicalRelPath(absolutePath, repoRoot);
8219
- const total_lines = source.split(/\r?\n/).length - (source.endsWith("\n") || source.endsWith("\r\n") ? 1 : 0);
8220
- const { descriptor } = parse2(source, { filename: absolutePath });
8221
- const allImports = [];
8222
- const allConstants = [];
8223
- const allFunctions = [];
8224
- const allClasses = [];
8495
+ const result = await this.scanBatch([absolutePath], repoRoot);
8496
+ return result.entries[0] ?? null;
8497
+ }
8498
+ async scanBatch(absolutePaths, repoRoot) {
8499
+ const interpreter = this.getInterpreter();
8500
+ const entries = [];
8225
8501
  const warnings = [];
8226
- const scriptBlocks = [descriptor.script, descriptor.scriptSetup].filter(Boolean);
8227
- if (scriptBlocks.length === 0) {
8228
- return {
8229
- path: relPath,
8230
- total_lines,
8231
- imports: [],
8232
- constants: [],
8233
- classes: [],
8234
- functions: []
8235
- };
8502
+ if (!interpreter) {
8503
+ const count = absolutePaths.length;
8504
+ warnings.push({
8505
+ path: "",
8506
+ message: `python interpreter not found on PATH; skipping ${count} .py file${count === 1 ? "" : "s"}`
8507
+ });
8508
+ return { entries, warnings };
8236
8509
  }
8237
- for (const block of scriptBlocks) {
8238
- if (!block)
8239
- continue;
8240
- if (block.lang === "ts" || block.lang === "tsx") {
8510
+ const scriptPath = ASSET.codeMapPython;
8511
+ const rand = Math.random().toString(36).slice(2);
8512
+ const listFile = join18(tmpdir(), `cdd-codemap-${process.pid}-${rand}.txt`);
8513
+ writeFileSync8(listFile, absolutePaths.join("\n") + "\n", "utf8");
8514
+ let stdout = "";
8515
+ let stderr = "";
8516
+ let exitCode = 0;
8517
+ try {
8518
+ const result = spawnSync2(
8519
+ interpreter,
8520
+ [scriptPath, "--batch-file", listFile, "--repo-root", repoRoot],
8521
+ {
8522
+ encoding: "utf8",
8523
+ timeout: TIMEOUT_MS,
8524
+ maxBuffer: 50 * 1024 * 1024
8525
+ // 50MB
8526
+ }
8527
+ );
8528
+ stdout = result.stdout ?? "";
8529
+ stderr = result.stderr ?? "";
8530
+ exitCode = result.status ?? -1;
8531
+ if (result.error) {
8532
+ const errMsg = result.error.message ?? String(result.error);
8533
+ if (errMsg.includes("ENOENT")) {
8534
+ warnings.push({
8535
+ path: "",
8536
+ message: `python interpreter not found (ENOENT); skipping ${absolutePaths.length} .py file(s)`
8537
+ });
8538
+ return { entries, warnings };
8539
+ }
8540
+ if (errMsg.includes("ETIMEDOUT") || errMsg.includes("timeout")) {
8541
+ warnings.push({
8542
+ path: "",
8543
+ message: `python scanner timed out after ${TIMEOUT_MS}ms; skipping .py files`
8544
+ });
8545
+ return { entries, warnings };
8546
+ }
8241
8547
  warnings.push({
8242
- path: relPath,
8243
- message: `skipping <script lang=${block.lang}> block (TypeScript not supported in v1)`
8548
+ path: "",
8549
+ message: `python scanner error: ${errMsg}; skipping .py files`
8244
8550
  });
8245
- continue;
8246
- }
8247
- const blockContent = block.content;
8248
- const lineOffset = block.loc.start.line;
8249
- const scriptEntry = parseJsSource(blockContent, relPath);
8250
- if (!scriptEntry) {
8251
- warnings.push({ path: relPath, message: "parse error in script block" });
8252
- continue;
8551
+ return { entries, warnings };
8253
8552
  }
8254
- const offset = lineOffset - 1;
8255
- for (const imp of scriptEntry.imports) {
8256
- allImports.push({ ...imp, line: imp.line + offset });
8553
+ } finally {
8554
+ try {
8555
+ unlinkSync(listFile);
8556
+ } catch {
8257
8557
  }
8258
- for (const c of scriptEntry.constants) {
8259
- allConstants.push({ ...c, line: c.line + offset });
8558
+ }
8559
+ if (exitCode === 3) {
8560
+ warnings.push({
8561
+ path: "",
8562
+ message: "python interpreter is < 3.9 (need ast.unparse); skipping .py files"
8563
+ });
8564
+ return { entries, warnings };
8565
+ }
8566
+ for (const line of stdout.split("\n")) {
8567
+ const trimmed = line.trim();
8568
+ if (!trimmed)
8569
+ continue;
8570
+ let parsed;
8571
+ try {
8572
+ parsed = JSON.parse(trimmed);
8573
+ } catch {
8574
+ continue;
8260
8575
  }
8261
- for (const fn of scriptEntry.functions) {
8262
- allFunctions.push({
8263
- ...fn,
8264
- lines: [fn.lines[0] + offset, fn.lines[1] + offset]
8265
- });
8576
+ if (!parsed.ok) {
8577
+ warnings.push({ path: parsed.path, message: parsed.error });
8578
+ continue;
8266
8579
  }
8267
- for (const cls of scriptEntry.classes) {
8268
- allClasses.push({
8269
- ...cls,
8270
- lines: [cls.lines[0] + offset, cls.lines[1] + offset],
8271
- methods: cls.methods.map((m) => ({
8272
- ...m,
8273
- lines: [m.lines[0] + offset, m.lines[1] + offset]
8580
+ const r = parsed;
8581
+ entries.push({
8582
+ path: canonicalRelPath(join18(repoRoot, r.path), repoRoot),
8583
+ total_lines: r.total_lines,
8584
+ imports: r.imports ?? [],
8585
+ constants: r.constants ?? [],
8586
+ classes: (r.classes ?? []).map((c) => ({
8587
+ name: c.name,
8588
+ lines: [c.lines[0], c.lines[1]],
8589
+ methods: (c.methods ?? []).map((m) => ({
8590
+ name: m.name,
8591
+ lines: [m.lines[0], m.lines[1]],
8592
+ async: m.async
8274
8593
  }))
8275
- });
8276
- }
8594
+ })),
8595
+ functions: (r.functions ?? []).map((f) => ({
8596
+ name: f.name,
8597
+ lines: [f.lines[0], f.lines[1]],
8598
+ decorators: f.decorators ?? [],
8599
+ async: f.async
8600
+ }))
8601
+ });
8277
8602
  }
8278
- return {
8279
- path: relPath,
8280
- total_lines,
8281
- imports: allImports,
8282
- constants: allConstants,
8283
- classes: allClasses,
8284
- functions: allFunctions
8285
- };
8603
+ if (exitCode === 2) {
8604
+ warnings.push({
8605
+ path: "",
8606
+ message: `python scanner exited with code 2 (fatal); partial results only. stderr: ${stderr.slice(0, 200)}`
8607
+ });
8608
+ }
8609
+ return { entries, warnings };
8286
8610
  }
8287
8611
  };
8288
- vueScanner = new VueScanner();
8289
- }
8290
- });
8291
-
8292
- // src/commands/code-map.ts
8293
- var code_map_exports = {};
8294
- __export(code_map_exports, {
8295
- codeMap: () => codeMap
8296
- });
8297
- import { existsSync as existsSync14, mkdirSync as mkdirSync6, readFileSync as readFileSync15, writeFileSync as writeFileSync7 } from "fs";
8298
- import { resolve, dirname as dirname4 } from "path";
8299
- import { createRequire } from "module";
8300
- import { fileURLToPath as fileURLToPath2 } from "url";
8301
- import { join as join17 } from "path";
8302
- async function codeMap(opts) {
8303
- const root = resolve(process.cwd(), opts.path);
8304
- const start = Date.now();
8305
- let cfg;
8306
- try {
8307
- cfg = loadCodeMapConfig(root);
8308
- } catch (err) {
8309
- log.error(`code-map: ${err.message}`);
8310
- return 1;
8311
- }
8312
- const include = [...cfg.include, ...opts.include];
8313
- const exclude = [...cfg.exclude, ...opts.exclude];
8314
- const files = walkRepo(root, { include, exclude });
8315
- const buckets = bucketByExtension(files);
8316
- const result = { entries: [], warnings: [] };
8317
- const tasks = [];
8318
- if (buckets[".py"]?.length) {
8319
- const { pythonScanner: pythonScanner2 } = await Promise.resolve().then(() => (init_python(), python_exports));
8320
- if (pythonScanner2.scanBatch) {
8321
- tasks.push(pythonScanner2.scanBatch(buckets[".py"], root));
8322
- }
8323
- }
8324
- const jsFiles = [
8325
- ...buckets[".js"] ?? [],
8326
- ...buckets[".jsx"] ?? [],
8327
- ...buckets[".mjs"] ?? [],
8328
- ...buckets[".cjs"] ?? []
8329
- ];
8330
- if (jsFiles.length) {
8331
- const { jsScanner: jsScanner2 } = await Promise.resolve().then(() => (init_javascript(), javascript_exports));
8332
- tasks.push(scanInProcess(jsScanner2, jsFiles, root));
8333
- }
8334
- const tsFiles = [
8335
- ...buckets[".ts"] ?? [],
8336
- ...buckets[".tsx"] ?? []
8337
- ];
8338
- if (tsFiles.length) {
8339
- const { tsScanner: tsScanner2 } = await Promise.resolve().then(() => (init_typescript(), typescript_exports));
8340
- tasks.push(scanInProcess(tsScanner2, tsFiles, root));
8341
- }
8342
- if (buckets[".vue"]?.length) {
8343
- const { vueScanner: vueScanner2 } = await Promise.resolve().then(() => (init_vue(), vue_exports));
8344
- tasks.push(scanInProcess(vueScanner2, buckets[".vue"], root));
8345
- }
8346
- for (const r of await Promise.all(tasks)) {
8347
- result.entries.push(...r.entries);
8348
- result.warnings.push(...r.warnings);
8349
- }
8350
- result.entries.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
8351
- for (const e of result.entries) {
8352
- if (e.total_lines > opts.maxLines) {
8353
- result.warnings.push({
8354
- path: e.path,
8355
- message: `file exceeds --max-lines (${e.total_lines} > ${opts.maxLines})`
8356
- });
8357
- }
8358
- }
8359
- const yamlBody = renderYaml(result.entries, { generator: `cdd-kit ${_pkg.version}` });
8360
- const totalSrc = result.entries.reduce((s, e) => s + e.total_lines, 0);
8361
- const mapLines = yamlBody.split("\n").length;
8362
- const compression = totalSrc === 0 ? 0 : totalSrc / mapLines;
8363
- const summaryLine = `scanned ${result.entries.length} files, ${totalSrc} src lines -> ${opts.out} (${mapLines} lines, compression ${compression.toFixed(1)}x)`;
8364
- for (const w of result.warnings) {
8365
- log.warn(`${w.path}: ${w.message}`);
8366
- }
8367
- if (opts.check) {
8368
- const existing = existsSync14(opts.out) ? readFileSync15(opts.out, "utf8") : "";
8369
- const normalize = (s) => s.replace(/^# generated: [^\n]+\n/m, "# generated: <normalized>\n");
8370
- if (normalize(existing) !== normalize(yamlBody)) {
8371
- log.error(`code-map out of date: ${opts.out} would change. Run \`cdd-kit code-map\` to regenerate.`);
8372
- return 1;
8373
- }
8374
- log.ok(`code-map up to date: ${opts.out}`);
8375
- return 0;
8376
- }
8377
- mkdirSync6(dirname4(opts.out), { recursive: true });
8378
- writeFileSync7(opts.out, yamlBody, "utf8");
8379
- log.ok(`${summaryLine} (${Date.now() - start}ms)`);
8380
- return 0;
8381
- }
8382
- var _require, _pkgPath, _pkg;
8383
- var init_code_map = __esm({
8384
- "src/commands/code-map.ts"() {
8385
- "use strict";
8386
- init_logger();
8387
- init_yaml_writer();
8388
- init_orchestrator();
8389
- init_config();
8390
- _require = createRequire(import.meta.url);
8391
- _pkgPath = join17(fileURLToPath2(import.meta.url), "..", "..", "..", "package.json");
8392
- _pkg = JSON.parse(readFileSync15(_pkgPath, "utf8"));
8612
+ pythonScanner = new PythonScanner();
8393
8613
  }
8394
8614
  });
8395
8615
 
8396
- // src/commands/doctor.ts
8397
- var doctor_exports = {};
8398
- __export(doctor_exports, {
8399
- doctor: () => doctor
8616
+ // src/code-map/scanners/javascript.ts
8617
+ var javascript_exports = {};
8618
+ __export(javascript_exports, {
8619
+ COMMON_PLUGINS: () => COMMON_PLUGINS,
8620
+ countSourceLines: () => countSourceLines,
8621
+ jsScanner: () => jsScanner,
8622
+ parseAndExtract: () => parseAndExtract,
8623
+ parseJsSource: () => parseJsSource,
8624
+ parseSourceWithPlugins: () => parseSourceWithPlugins
8400
8625
  });
8401
- import { existsSync as existsSync15, readdirSync as readdirSync8, readFileSync as readFileSync16 } from "fs";
8402
- import { createHash as createHash4 } from "crypto";
8403
- import { join as join18 } from "path";
8404
- function fileExists(cwd, relPath) {
8405
- return existsSync15(join18(cwd, relPath));
8406
- }
8407
- function findFiles(dir, predicate, found = []) {
8408
- if (!existsSync15(dir))
8409
- return found;
8410
- for (const entry of readdirSync8(dir, { withFileTypes: true })) {
8411
- const fullPath = join18(dir, entry.name);
8412
- if (entry.isDirectory())
8413
- findFiles(fullPath, predicate, found);
8414
- else if (entry.isFile() && predicate(entry.name))
8415
- found.push(fullPath);
8416
- }
8417
- return found;
8626
+ import { readFileSync as readFileSync14 } from "fs";
8627
+ import { parse } from "@babel/parser";
8628
+ function parseSourceWithPlugins(source, plugins) {
8629
+ return parse(source, {
8630
+ sourceType: "unambiguous",
8631
+ allowReturnOutsideFunction: true,
8632
+ allowAwaitOutsideFunction: true,
8633
+ errorRecovery: true,
8634
+ plugins
8635
+ });
8418
8636
  }
8419
- function sha256OfFile3(path) {
8420
- try {
8421
- return createHash4("sha256").update(readFileSync16(path)).digest("hex");
8422
- } catch {
8423
- return "";
8424
- }
8637
+ function isCallExpression(node, callee) {
8638
+ if (!node || node.type !== "CallExpression")
8639
+ return false;
8640
+ const c = node.callee;
8641
+ return c.type === "Identifier" && c.name === callee;
8425
8642
  }
8426
- function inputDigest(paths) {
8427
- const combined = paths.slice().sort().map((p) => `${p}:${sha256OfFile3(p)}`).join("\n");
8428
- return createHash4("sha256").update(combined).digest("hex");
8643
+ function extractRequireModule(node) {
8644
+ if (!node || node.type !== "CallExpression")
8645
+ return null;
8646
+ const c = node.callee;
8647
+ if (c.type !== "Identifier" || c.name !== "require")
8648
+ return null;
8649
+ const arg = node.arguments[0];
8650
+ if (!arg || arg.type !== "StringLiteral")
8651
+ return null;
8652
+ return arg.value;
8429
8653
  }
8430
- function readContextIndexMetadata(filePath) {
8431
- if (!existsSync15(filePath))
8432
- return {};
8433
- const text = readFileSync16(filePath, "utf8");
8434
- const out = {};
8435
- const digestMatch = text.match(/^inputs-digest:\s*([a-f0-9]+)/m);
8436
- if (digestMatch)
8437
- out.inputsDigest = digestMatch[1];
8438
- const missingMatch = text.match(/^missing-summary-count:\s*(\d+)/m);
8439
- if (missingMatch)
8440
- out.missingSummary = Number(missingMatch[1]);
8441
- return out;
8654
+ function getLineRange(node) {
8655
+ const start = node.loc?.start.line ?? 1;
8656
+ const end = node.loc?.end.line ?? start;
8657
+ return [start, end];
8442
8658
  }
8443
- function checkContextFreshness(cwd) {
8444
- const findings = [];
8445
- const projectMap = join18(cwd, "specs", "context", "project-map.md");
8446
- const contractsIndex = join18(cwd, "specs", "context", "contracts-index.md");
8447
- const contextPolicy = join18(cwd, ".cdd", "context-policy.json");
8448
- const contractFiles = findFiles(
8449
- join18(cwd, "contracts"),
8450
- (name) => name.endsWith(".md") && name !== "INDEX.md" && name !== "CHANGELOG.md"
8451
- );
8452
- if (!existsSync15(projectMap) || !existsSync15(contractsIndex)) {
8453
- findings.push({
8454
- level: "warning",
8455
- message: "specs/context indexes are missing; run cdd-kit context-scan before classification"
8456
- });
8457
- return findings;
8458
- }
8459
- const projectMapMeta = readContextIndexMetadata(projectMap);
8460
- const contractsIndexMeta = readContextIndexMetadata(contractsIndex);
8461
- const projectInputDigest = inputDigest([contextPolicy].filter(existsSync15));
8462
- if (projectMapMeta.inputsDigest === void 0) {
8463
- findings.push({
8464
- level: "warning",
8465
- message: "specs/context/project-map.md was generated by an older cdd-kit (no inputs-digest); re-run cdd-kit context-scan"
8466
- });
8467
- } else if (projectInputDigest && projectMapMeta.inputsDigest !== projectInputDigest) {
8468
- findings.push({
8469
- level: "warning",
8470
- message: "specs/context/project-map.md inputs changed (.cdd/context-policy.json); re-run cdd-kit context-scan"
8471
- });
8472
- }
8473
- const contractsInputDigest = inputDigest(contractFiles);
8474
- if (contractsIndexMeta.inputsDigest === void 0) {
8475
- findings.push({
8476
- level: "warning",
8477
- message: "specs/context/contracts-index.md was generated by an older cdd-kit (no inputs-digest); re-run cdd-kit context-scan"
8478
- });
8479
- } else if (contractsInputDigest && contractsIndexMeta.inputsDigest !== contractsInputDigest) {
8480
- findings.push({
8481
- level: "warning",
8482
- message: "specs/context/contracts-index.md inputs changed (contracts/*); re-run cdd-kit context-scan"
8483
- });
8484
- }
8485
- if (contractsIndexMeta.missingSummary !== void 0 && contractsIndexMeta.missingSummary > 0) {
8486
- findings.push({
8487
- level: "warning",
8488
- message: `contracts-index reports ${contractsIndexMeta.missingSummary} contract(s) without deterministic summary metadata`
8489
- });
8490
- }
8491
- if (findings.length === 0) {
8492
- findings.push({ level: "ok", message: "context indexes are present and fresh" });
8659
+ function processImportDeclaration(node) {
8660
+ const items = [];
8661
+ for (const spec of node.specifiers) {
8662
+ if (spec.type === "ImportDefaultSpecifier") {
8663
+ items.push(`default:${spec.local.name}`);
8664
+ } else if (spec.type === "ImportNamespaceSpecifier") {
8665
+ items.push(`*:${spec.local.name}`);
8666
+ } else if (spec.type === "ImportSpecifier") {
8667
+ const imported = spec.imported;
8668
+ items.push(imported.type === "Identifier" ? imported.name : spec.local.name);
8669
+ }
8493
8670
  }
8494
- return findings;
8671
+ return {
8672
+ module: node.source.value,
8673
+ items,
8674
+ line: node.loc?.start.line ?? 1
8675
+ };
8495
8676
  }
8496
- function readAgentModel(path) {
8497
- try {
8498
- const text = readFileSync16(path, "utf8");
8499
- const m = text.match(/^model:\s*(\S+)/m);
8500
- return m ? m[1] : null;
8501
- } catch {
8677
+ function processFunctionDeclaration(node, nameOverride) {
8678
+ const name = nameOverride ?? node.id?.name;
8679
+ if (!name)
8502
8680
  return null;
8503
- }
8681
+ return {
8682
+ name,
8683
+ lines: getLineRange(node),
8684
+ decorators: [],
8685
+ async: node.async
8686
+ };
8504
8687
  }
8505
- function checkModelPolicyDrift(cwd) {
8506
- const policyPath = join18(cwd, ".cdd", "model-policy.json");
8507
- if (!existsSync15(policyPath))
8508
- return [];
8509
- let policy;
8510
- try {
8511
- policy = JSON.parse(readFileSync16(policyPath, "utf8"));
8512
- } catch {
8513
- return [{ level: "warning", message: ".cdd/model-policy.json is not valid JSON" }];
8514
- }
8515
- const roles = policy.roles ?? {};
8516
- if (Object.keys(roles).length === 0) {
8517
- return [{
8518
- level: "warning",
8519
- message: ".cdd/model-policy.json has no role bindings; run cdd-kit upgrade to install defaults"
8520
- }];
8521
- }
8522
- const candidateDirs = [
8523
- join18(cwd, ".claude", "agents"),
8524
- process.env.HOME ? join18(process.env.HOME, ".claude", "agents") : "",
8525
- process.env.USERPROFILE ? join18(process.env.USERPROFILE, ".claude", "agents") : ""
8526
- ].filter((p) => p && existsSync15(p));
8527
- if (candidateDirs.length === 0)
8528
- return [];
8529
- const findings = [];
8530
- for (const [role, expected] of Object.entries(roles)) {
8531
- let foundAny = false;
8532
- for (const dir of candidateDirs) {
8533
- const path = join18(dir, `${role}.md`);
8534
- if (!existsSync15(path))
8535
- continue;
8536
- foundAny = true;
8537
- const actual = readAgentModel(path);
8538
- if (actual && actual !== expected) {
8539
- findings.push({
8540
- level: "warning",
8541
- message: `model-policy drift: ${role} expected ${expected}, agent prompt uses ${actual} (${path})`
8542
- });
8688
+ function processClassDeclaration(node, nameOverride) {
8689
+ const name = nameOverride ?? node.id?.name;
8690
+ if (!name)
8691
+ return null;
8692
+ const methods = [];
8693
+ for (const member of node.body.body) {
8694
+ if (member.type === "ClassMethod" || member.type === "ClassPrivateMethod") {
8695
+ const m = member;
8696
+ let methodName;
8697
+ if (m.key.type === "Identifier") {
8698
+ methodName = m.key.name;
8699
+ } else if (m.key.type === "PrivateName") {
8700
+ methodName = `#${m.key.id.name}`;
8701
+ } else {
8702
+ methodName = "<computed>";
8543
8703
  }
8704
+ methods.push({
8705
+ name: methodName,
8706
+ lines: getLineRange(m),
8707
+ async: m.async
8708
+ });
8544
8709
  }
8545
- if (!foundAny) {
8546
- }
8547
- }
8548
- if (findings.length === 0) {
8549
- findings.push({ level: "ok", message: "model-policy roles match installed agent prompts" });
8550
8710
  }
8551
- return findings;
8711
+ return {
8712
+ name,
8713
+ lines: getLineRange(node),
8714
+ methods
8715
+ };
8552
8716
  }
8553
- async function checkAgentLint(cwd) {
8554
- const agentsDir = join18(cwd, ".claude", "agents");
8555
- if (!existsSync15(agentsDir))
8556
- return [];
8557
- try {
8558
- const { readdirSync: rds, readFileSync: rfs } = await import("fs");
8559
- const files = rds(agentsDir).filter((f) => f.endsWith(".md"));
8560
- if (files.length === 0)
8561
- return [];
8562
- const findings = [];
8563
- for (const filename of files) {
8564
- const content = rfs(join18(agentsDir, filename), "utf8");
8565
- const artifactsSection = content.match(
8566
- /### Required artifacts for this agent\s*\n[\s\S]*?(?=\n#{2,3} |\n---|\s*$)/
8567
- )?.[0];
8568
- if (!artifactsSection || !/```ya?ml\s*\nartifacts:/.test(artifactsSection)) {
8569
- findings.push({
8570
- level: "warning",
8571
- message: `lint-agents: ${filename}: missing artifacts YAML block in Required artifacts section`
8572
- });
8573
- }
8574
- const readScopeCount = (content.match(/^## Read scope\s*$/gm) ?? []).length;
8575
- if (readScopeCount > 1) {
8576
- findings.push({
8577
- level: "warning",
8578
- message: `lint-agents: ${filename}: duplicate ## Read scope headings (${readScopeCount})`
8579
- });
8717
+ function processVariableDeclaration(node, imports, constants, functions) {
8718
+ for (const decl of node.declarations) {
8719
+ if (!decl.id || decl.id.type !== "Identifier")
8720
+ continue;
8721
+ const varName = decl.id.name;
8722
+ const init2 = decl.init;
8723
+ if (init2 && isCallExpression(init2, "require")) {
8724
+ const mod = extractRequireModule(init2);
8725
+ if (mod) {
8726
+ imports.push({ module: mod, items: [`default:${varName}`], line: node.loc?.start.line ?? 1 });
8580
8727
  }
8728
+ continue;
8581
8729
  }
8582
- if (findings.length === 0) {
8583
- findings.push({ level: "ok", message: "lint-agents: all agent prompts pass shape checks" });
8730
+ if (isAllCapsConst(varName) && init2 !== null && init2 !== void 0) {
8731
+ constants.push({ name: varName, line: node.loc?.start.line ?? 1 });
8732
+ continue;
8584
8733
  }
8585
- return findings;
8586
- } catch {
8587
- return [];
8734
+ if (init2 && (init2.type === "ArrowFunctionExpression" || init2.type === "FunctionExpression")) {
8735
+ functions.push({
8736
+ name: varName,
8737
+ lines: getLineRange(node),
8738
+ decorators: [],
8739
+ async: init2.async
8740
+ });
8741
+ } else if (init2 && init2.type === "CallExpression" && /^[A-Z]/.test(varName)) {
8742
+ functions.push({
8743
+ name: varName,
8744
+ lines: getLineRange(node),
8745
+ decorators: [],
8746
+ async: false
8747
+ });
8748
+ }
8749
+ }
8750
+ }
8751
+ function processTsInterfaceDeclaration(node, exported) {
8752
+ if (!node || node.type !== "TSInterfaceDeclaration")
8753
+ return null;
8754
+ if (!node.id || node.id.type !== "Identifier")
8755
+ return null;
8756
+ return {
8757
+ name: node.id.name,
8758
+ lines: getLineRange(node),
8759
+ exported
8760
+ };
8761
+ }
8762
+ function processTsTypeAliasDeclaration(node, exported) {
8763
+ if (!node || node.type !== "TSTypeAliasDeclaration")
8764
+ return null;
8765
+ if (!node.id || node.id.type !== "Identifier")
8766
+ return null;
8767
+ return {
8768
+ name: node.id.name,
8769
+ lines: getLineRange(node),
8770
+ exported
8771
+ };
8772
+ }
8773
+ function processTsEnumDeclaration(node, exported) {
8774
+ if (!node || node.type !== "TSEnumDeclaration")
8775
+ return null;
8776
+ if (!node.id || node.id.type !== "Identifier")
8777
+ return null;
8778
+ const members = [];
8779
+ for (const m of node.members ?? []) {
8780
+ if (!m || !m.id)
8781
+ continue;
8782
+ if (m.id.type === "Identifier")
8783
+ members.push(m.id.name);
8784
+ else if (m.id.type === "StringLiteral")
8785
+ members.push(m.id.value);
8588
8786
  }
8787
+ return {
8788
+ name: node.id.name,
8789
+ lines: getLineRange(node),
8790
+ exported,
8791
+ members
8792
+ };
8589
8793
  }
8590
- function checkCodeMap(cwd) {
8591
- const findings = [];
8592
- const mapPath = join18(cwd, ".cdd", "code-map.yml");
8593
- if (!existsSync15(mapPath)) {
8594
- const probe2 = checkCodeMapFreshness(cwd);
8595
- if (probe2.status === "config-error") {
8596
- findings.push({ level: "warning", message: `.cdd/code-map-config.yml is invalid: ${probe2.configError}` });
8597
- } else if (probe2.status === "missing-with-sources") {
8598
- findings.push({ level: "warning", message: ".cdd/code-map.yml is missing; run `cdd-kit code-map`" });
8794
+ function processStatement(stmt, buckets, extractTsTypes, exportedFromWrapper = false) {
8795
+ if (stmt.type === "ExportNamedDeclaration" && stmt.declaration) {
8796
+ processStatement(stmt.declaration, buckets, extractTsTypes, true);
8797
+ return;
8798
+ }
8799
+ if (stmt.type === "ExportDefaultDeclaration") {
8800
+ const decl = stmt.declaration;
8801
+ if (decl.type === "FunctionDeclaration") {
8802
+ const fe = processFunctionDeclaration(decl, decl.id?.name ?? "default");
8803
+ if (fe)
8804
+ buckets.functions.push(fe);
8805
+ } else if (decl.type === "ClassDeclaration") {
8806
+ const ce = processClassDeclaration(decl, decl.id?.name ?? "default");
8807
+ if (ce)
8808
+ buckets.classes.push(ce);
8599
8809
  }
8600
- return findings;
8810
+ return;
8601
8811
  }
8602
- const probe = checkCodeMapFreshness(cwd);
8603
- if (probe.status === "config-error") {
8604
- findings.push({ level: "warning", message: `.cdd/code-map-config.yml is invalid: ${probe.configError}` });
8605
- return findings;
8812
+ if (stmt.type === "ImportDeclaration") {
8813
+ buckets.imports.push(processImportDeclaration(stmt));
8814
+ return;
8606
8815
  }
8607
- if (probe.status === "stale") {
8608
- const top = probe.staleFiles.slice(0, 3).join(", ");
8609
- const more = probe.staleCount > 3 ? ` (+${probe.staleCount - 3} more)` : "";
8610
- findings.push({ level: "warning", message: `code-map stale: ${top}${more}; run \`cdd-kit code-map\`` });
8816
+ if (stmt.type === "FunctionDeclaration") {
8817
+ const fe = processFunctionDeclaration(stmt);
8818
+ if (fe)
8819
+ buckets.functions.push(fe);
8820
+ return;
8611
8821
  }
8612
- const text = readFileSync16(mapPath, "utf8");
8613
- const m = text.match(/^# files: (\d+), src-lines: (\d+), map-lines: (\d+), compression: ([\d.]+)x/m);
8614
- if (m) {
8615
- findings.push({ level: "ok", message: `code-map: ${m[1]} files, ${m[4]}x compression` });
8616
- } else {
8617
- findings.push({ level: "warning", message: ".cdd/code-map.yml has no metadata header (regenerate with cdd-kit 2.0.5+)" });
8822
+ if (stmt.type === "ClassDeclaration") {
8823
+ const ce = processClassDeclaration(stmt);
8824
+ if (ce)
8825
+ buckets.classes.push(ce);
8826
+ return;
8618
8827
  }
8619
- return findings;
8620
- }
8621
- async function buildDoctorReport(cwd, opts) {
8622
- const requestedProvider = opts.provider ?? "auto";
8623
- if (!validateProviderOption(requestedProvider)) {
8624
- log.error(`Invalid provider: ${requestedProvider}. Use auto, claude, codex, or both.`);
8625
- process.exit(1);
8828
+ if (stmt.type === "VariableDeclaration") {
8829
+ processVariableDeclaration(stmt, buckets.imports, buckets.constants, buckets.functions);
8830
+ return;
8626
8831
  }
8627
- const strict = opts.strict ?? false;
8628
- const provider = inferProvider(cwd, requestedProvider);
8629
- const findings = [];
8630
- for (const relPath of ["contracts", "specs/templates", ".cdd/context-policy.json", ".cdd/model-policy.json"]) {
8631
- findings.push(fileExists(cwd, relPath) ? { level: "ok", message: `${relPath} exists` } : { level: "warning", message: `${relPath} is missing; run cdd-kit upgrade --yes` });
8832
+ if (extractTsTypes) {
8833
+ const anyStmt = stmt;
8834
+ if (anyStmt.type === "TSInterfaceDeclaration") {
8835
+ const e = processTsInterfaceDeclaration(anyStmt, exportedFromWrapper);
8836
+ if (e)
8837
+ buckets.interfaces.push(e);
8838
+ return;
8839
+ }
8840
+ if (anyStmt.type === "TSTypeAliasDeclaration") {
8841
+ const e = processTsTypeAliasDeclaration(anyStmt, exportedFromWrapper);
8842
+ if (e)
8843
+ buckets.types.push(e);
8844
+ return;
8845
+ }
8846
+ if (anyStmt.type === "TSEnumDeclaration") {
8847
+ const e = processTsEnumDeclaration(anyStmt, exportedFromWrapper);
8848
+ if (e)
8849
+ buckets.enums.push(e);
8850
+ return;
8851
+ }
8632
8852
  }
8633
- if ((provider === "claude" || provider === "both") && !fileExists(cwd, "CLAUDE.md")) {
8634
- findings.push({ level: "warning", message: "CLAUDE.md is missing for Claude provider; run cdd-kit upgrade --provider claude --yes" });
8853
+ if (stmt.type === "ExpressionStatement") {
8854
+ const expr = stmt.expression;
8855
+ if (isCallExpression(expr, "require")) {
8856
+ const mod = extractRequireModule(expr);
8857
+ if (mod) {
8858
+ buckets.imports.push({ module: mod, items: [], line: stmt.loc?.start.line ?? 1 });
8859
+ }
8860
+ }
8635
8861
  }
8636
- if ((provider === "claude" || provider === "both") && !fileExists(cwd, "AGENTS.md")) {
8637
- findings.push({ level: "warning", message: "AGENTS.md is missing for Claude provider; run cdd-kit upgrade --provider claude --yes" });
8862
+ }
8863
+ function parseAndExtract(source, opts) {
8864
+ let ast;
8865
+ try {
8866
+ ast = parseSourceWithPlugins(source, opts.plugins);
8867
+ } catch {
8868
+ return null;
8638
8869
  }
8639
- if ((provider === "codex" || provider === "both") && !fileExists(cwd, "CODEX.md")) {
8640
- findings.push({ level: "warning", message: "CODEX.md is missing for Codex provider; run cdd-kit upgrade --provider codex --yes" });
8870
+ const buckets = {
8871
+ imports: [],
8872
+ constants: [],
8873
+ functions: [],
8874
+ classes: [],
8875
+ interfaces: [],
8876
+ types: [],
8877
+ enums: []
8878
+ };
8879
+ for (const stmt of ast.program.body) {
8880
+ processStatement(stmt, buckets, !!opts.extractTsTypes);
8641
8881
  }
8642
- findings.push(...checkContextFreshness(cwd));
8643
- findings.push(...checkModelPolicyDrift(cwd));
8644
- findings.push(...await checkAgentLint(cwd));
8645
- findings.push(...checkCodeMap(cwd));
8646
- const errors = findings.filter((finding) => finding.level === "error").length;
8647
- const warnings = findings.filter((finding) => finding.level === "warning").length;
8882
+ return buckets;
8883
+ }
8884
+ function countSourceLines(source) {
8885
+ if (source === "")
8886
+ return 0;
8887
+ return source.split(/\r?\n/).length - (source.endsWith("\n") || source.endsWith("\r\n") ? 1 : 0);
8888
+ }
8889
+ function parseJsSource(source, relPath) {
8890
+ const total_lines = countSourceLines(source);
8891
+ const r = parseAndExtract(source, { plugins: JS_PLUGINS, extractTsTypes: false });
8892
+ if (!r)
8893
+ return null;
8648
8894
  return {
8649
- provider,
8650
- strict,
8651
- findings,
8652
- errors,
8653
- warnings,
8654
- ok: errors === 0 && (!strict || warnings === 0)
8895
+ path: relPath,
8896
+ total_lines,
8897
+ imports: r.imports,
8898
+ constants: r.constants,
8899
+ classes: r.classes,
8900
+ functions: r.functions
8655
8901
  };
8656
8902
  }
8657
- async function attemptAutoFixes(cwd, report) {
8658
- const fixed = [];
8659
- const remaining = [];
8660
- for (const finding of report.findings) {
8661
- if (finding.level !== "warning") {
8662
- remaining.push(finding);
8663
- continue;
8664
- }
8665
- if (/specs\/context indexes are missing|inputs changed|older cdd-kit|older than/i.test(finding.message)) {
8666
- try {
8667
- const { contextScan: contextScan2 } = await Promise.resolve().then(() => (init_context_scan(), context_scan_exports));
8668
- await contextScan2();
8669
- fixed.push(`ran context-scan to refresh specs/context/`);
8670
- continue;
8671
- } catch (err) {
8672
- remaining.push({ level: "warning", message: `${finding.message} (auto-fix failed: ${err.message})` });
8673
- continue;
8903
+ var COMMON_PLUGINS, JS_PLUGINS, JavaScriptScanner, jsScanner;
8904
+ var init_javascript = __esm({
8905
+ "src/code-map/scanners/javascript.ts"() {
8906
+ "use strict";
8907
+ init_common();
8908
+ COMMON_PLUGINS = [
8909
+ "classProperties",
8910
+ "classPrivateProperties",
8911
+ "classPrivateMethods",
8912
+ "decorators-legacy",
8913
+ "topLevelAwait",
8914
+ "dynamicImport",
8915
+ "optionalChaining",
8916
+ "nullishCoalescingOperator",
8917
+ "objectRestSpread",
8918
+ "asyncGenerators",
8919
+ "numericSeparator",
8920
+ "logicalAssignment"
8921
+ ];
8922
+ JS_PLUGINS = ["jsx", ...COMMON_PLUGINS];
8923
+ JavaScriptScanner = class {
8924
+ extensions = [".js"];
8925
+ async scan(absolutePath, repoRoot) {
8926
+ let source;
8927
+ try {
8928
+ source = readFileSync14(absolutePath, "utf8");
8929
+ } catch (err) {
8930
+ throw err;
8931
+ }
8932
+ if (isBinary(source)) {
8933
+ return null;
8934
+ }
8935
+ const relPath = canonicalRelPath(absolutePath, repoRoot);
8936
+ return parseJsSource(source, relPath);
8674
8937
  }
8675
- }
8676
- if (/code-map stale|code-map\.yml is missing/i.test(finding.message)) {
8677
- try {
8678
- const { codeMap: codeMap2 } = await Promise.resolve().then(() => (init_code_map(), code_map_exports));
8679
- await codeMap2({ path: ".", out: ".cdd/code-map.yml", include: [], exclude: [], check: false, maxLines: 1e5 });
8680
- fixed.push("regenerated .cdd/code-map.yml");
8681
- continue;
8682
- } catch (err) {
8683
- remaining.push({ level: "warning", message: `${finding.message} (auto-fix failed: ${err.message})` });
8684
- continue;
8938
+ };
8939
+ jsScanner = new JavaScriptScanner();
8940
+ }
8941
+ });
8942
+
8943
+ // src/code-map/scanners/typescript.ts
8944
+ var typescript_exports = {};
8945
+ __export(typescript_exports, {
8946
+ tsScanner: () => tsScanner
8947
+ });
8948
+ import { readFileSync as readFileSync15 } from "fs";
8949
+ var TypeScriptScanner, tsScanner;
8950
+ var init_typescript = __esm({
8951
+ "src/code-map/scanners/typescript.ts"() {
8952
+ "use strict";
8953
+ init_common();
8954
+ init_javascript();
8955
+ TypeScriptScanner = class {
8956
+ extensions = [".ts", ".tsx"];
8957
+ async scan(absolutePath, repoRoot) {
8958
+ let source;
8959
+ try {
8960
+ source = readFileSync15(absolutePath, "utf8");
8961
+ } catch (err) {
8962
+ throw err;
8963
+ }
8964
+ if (isBinary(source)) {
8965
+ return null;
8966
+ }
8967
+ const isTsx = absolutePath.toLowerCase().endsWith(".tsx");
8968
+ const plugins = isTsx ? ["typescript", "jsx", ...COMMON_PLUGINS] : ["typescript", ...COMMON_PLUGINS];
8969
+ const r = parseAndExtract(source, { plugins, extractTsTypes: true });
8970
+ if (!r)
8971
+ return null;
8972
+ const relPath = canonicalRelPath(absolutePath, repoRoot);
8973
+ const total_lines = countSourceLines(source);
8974
+ return {
8975
+ path: relPath,
8976
+ total_lines,
8977
+ imports: r.imports,
8978
+ constants: r.constants,
8979
+ classes: r.classes,
8980
+ functions: r.functions,
8981
+ interfaces: r.interfaces,
8982
+ types: r.types,
8983
+ enums: r.enums
8984
+ };
8685
8985
  }
8686
- }
8687
- if (/model-policy\.json has no role bindings/i.test(finding.message)) {
8688
- const policyPath = join18(cwd, ".cdd", "model-policy.json");
8689
- try {
8690
- let existing = {};
8986
+ };
8987
+ tsScanner = new TypeScriptScanner();
8988
+ }
8989
+ });
8990
+
8991
+ // src/code-map/scanners/vue.ts
8992
+ var vue_exports = {};
8993
+ __export(vue_exports, {
8994
+ vueScanner: () => vueScanner
8995
+ });
8996
+ import { readFileSync as readFileSync16 } from "fs";
8997
+ import { parse as parse2 } from "@vue/compiler-sfc";
8998
+ var VueScanner, vueScanner;
8999
+ var init_vue = __esm({
9000
+ "src/code-map/scanners/vue.ts"() {
9001
+ "use strict";
9002
+ init_common();
9003
+ init_javascript();
9004
+ VueScanner = class {
9005
+ extensions = [".vue"];
9006
+ async scan(absolutePath, repoRoot) {
9007
+ let source;
8691
9008
  try {
8692
- existing = JSON.parse(readFileSync16(policyPath, "utf8"));
8693
- } catch {
9009
+ source = readFileSync16(absolutePath, "utf8");
9010
+ } catch (err) {
9011
+ throw err;
8694
9012
  }
8695
- const merged = {
8696
- ...existing,
8697
- roles: {
8698
- "change-classifier": "claude-opus-4-7",
8699
- "spec-architect": "claude-opus-4-7",
8700
- "qa-reviewer": "claude-opus-4-7",
8701
- "contract-reviewer": "claude-sonnet-4-6",
8702
- "test-strategist": "claude-sonnet-4-6",
8703
- "backend-engineer": "claude-sonnet-4-6",
8704
- "frontend-engineer": "claude-sonnet-4-6",
8705
- "ci-cd-gatekeeper": "claude-sonnet-4-6",
8706
- "e2e-resilience-engineer": "claude-sonnet-4-6",
8707
- "monkey-test-engineer": "claude-sonnet-4-6",
8708
- "stress-soak-engineer": "claude-sonnet-4-6",
8709
- "ui-ux-reviewer": "claude-sonnet-4-6",
8710
- "visual-reviewer": "claude-haiku-4-5-20251001",
8711
- "dependency-security-reviewer": "claude-sonnet-4-6",
8712
- "spec-drift-auditor": "claude-opus-4-7",
8713
- "repo-context-scanner": "claude-haiku-4-5-20251001"
9013
+ if (isBinary(source)) {
9014
+ return null;
9015
+ }
9016
+ const relPath = canonicalRelPath(absolutePath, repoRoot);
9017
+ const total_lines = source.split(/\r?\n/).length - (source.endsWith("\n") || source.endsWith("\r\n") ? 1 : 0);
9018
+ const { descriptor } = parse2(source, { filename: absolutePath });
9019
+ const allImports = [];
9020
+ const allConstants = [];
9021
+ const allFunctions = [];
9022
+ const allClasses = [];
9023
+ const warnings = [];
9024
+ const scriptBlocks = [descriptor.script, descriptor.scriptSetup].filter(Boolean);
9025
+ if (scriptBlocks.length === 0) {
9026
+ return {
9027
+ path: relPath,
9028
+ total_lines,
9029
+ imports: [],
9030
+ constants: [],
9031
+ classes: [],
9032
+ functions: []
9033
+ };
9034
+ }
9035
+ for (const block of scriptBlocks) {
9036
+ if (!block)
9037
+ continue;
9038
+ if (block.lang === "ts" || block.lang === "tsx") {
9039
+ warnings.push({
9040
+ path: relPath,
9041
+ message: `skipping <script lang=${block.lang}> block (TypeScript not supported in v1)`
9042
+ });
9043
+ continue;
9044
+ }
9045
+ const blockContent = block.content;
9046
+ const lineOffset = block.loc.start.line;
9047
+ const scriptEntry = parseJsSource(blockContent, relPath);
9048
+ if (!scriptEntry) {
9049
+ warnings.push({ path: relPath, message: "parse error in script block" });
9050
+ continue;
9051
+ }
9052
+ const offset = lineOffset - 1;
9053
+ for (const imp of scriptEntry.imports) {
9054
+ allImports.push({ ...imp, line: imp.line + offset });
9055
+ }
9056
+ for (const c of scriptEntry.constants) {
9057
+ allConstants.push({ ...c, line: c.line + offset });
9058
+ }
9059
+ for (const fn of scriptEntry.functions) {
9060
+ allFunctions.push({
9061
+ ...fn,
9062
+ lines: [fn.lines[0] + offset, fn.lines[1] + offset]
9063
+ });
9064
+ }
9065
+ for (const cls of scriptEntry.classes) {
9066
+ allClasses.push({
9067
+ ...cls,
9068
+ lines: [cls.lines[0] + offset, cls.lines[1] + offset],
9069
+ methods: cls.methods.map((m) => ({
9070
+ ...m,
9071
+ lines: [m.lines[0] + offset, m.lines[1] + offset]
9072
+ }))
9073
+ });
8714
9074
  }
9075
+ }
9076
+ return {
9077
+ path: relPath,
9078
+ total_lines,
9079
+ imports: allImports,
9080
+ constants: allConstants,
9081
+ classes: allClasses,
9082
+ functions: allFunctions
8715
9083
  };
8716
- const { writeFileSync: writeFileSync13 } = await import("fs");
8717
- writeFileSync13(policyPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
8718
- fixed.push(`populated .cdd/model-policy.json with default role bindings`);
8719
- continue;
8720
- } catch (err) {
8721
- remaining.push({ level: "warning", message: `${finding.message} (auto-fix failed: ${err.message})` });
8722
- continue;
8723
9084
  }
8724
- }
8725
- if (/\.cdd\/.*is missing|run cdd-kit upgrade/i.test(finding.message)) {
8726
- remaining.push({
8727
- level: "warning",
8728
- message: `${finding.message} (run \`cdd-kit upgrade --yes\` manually \u2014 too invasive for --fix)`
8729
- });
8730
- continue;
8731
- }
8732
- remaining.push(finding);
9085
+ };
9086
+ vueScanner = new VueScanner();
8733
9087
  }
8734
- return { fixed, remaining };
8735
- }
8736
- async function doctor(opts = {}) {
8737
- const cwd = process.cwd();
8738
- let report = await buildDoctorReport(cwd, opts);
8739
- if (opts.fix && !opts.json) {
8740
- log.blank();
8741
- log.info("Doctor --fix: attempting safe auto-resolutions\u2026");
8742
- const { fixed, remaining } = await attemptAutoFixes(cwd, report);
8743
- for (const f of fixed)
8744
- log.ok(`fixed: ${f}`);
8745
- if (fixed.length > 0) {
8746
- report = await buildDoctorReport(cwd, opts);
8747
- } else {
8748
- log.info("no auto-fixable findings");
9088
+ });
9089
+
9090
+ // src/commands/code-map.ts
9091
+ var code_map_exports = {};
9092
+ __export(code_map_exports, {
9093
+ codeMap: () => codeMap
9094
+ });
9095
+ import { existsSync as existsSync16, mkdirSync as mkdirSync8, readFileSync as readFileSync17, writeFileSync as writeFileSync9 } from "fs";
9096
+ import { resolve, dirname as dirname5 } from "path";
9097
+ import { createRequire } from "module";
9098
+ import { fileURLToPath as fileURLToPath2 } from "url";
9099
+ import { join as join19 } from "path";
9100
+ async function codeMap(opts) {
9101
+ const root = resolve(process.cwd(), opts.path);
9102
+ const start = Date.now();
9103
+ let cfg;
9104
+ try {
9105
+ cfg = loadCodeMapConfig(root);
9106
+ } catch (err) {
9107
+ log.error(`code-map: ${err.message}`);
9108
+ return 1;
9109
+ }
9110
+ const include = [...cfg.include, ...opts.include];
9111
+ const exclude = [...cfg.exclude, ...opts.exclude];
9112
+ const files = walkRepo(root, { include, exclude });
9113
+ const buckets = bucketByExtension(files);
9114
+ const result = { entries: [], warnings: [] };
9115
+ const tasks = [];
9116
+ if (buckets[".py"]?.length) {
9117
+ const { pythonScanner: pythonScanner2 } = await Promise.resolve().then(() => (init_python(), python_exports));
9118
+ if (pythonScanner2.scanBatch) {
9119
+ tasks.push(pythonScanner2.scanBatch(buckets[".py"], root));
8749
9120
  }
8750
9121
  }
8751
- if (opts.json) {
8752
- console.log(JSON.stringify(report, null, 2));
8753
- if (!report.ok)
8754
- process.exit(1);
8755
- return;
9122
+ const jsFiles = [
9123
+ ...buckets[".js"] ?? [],
9124
+ ...buckets[".jsx"] ?? [],
9125
+ ...buckets[".mjs"] ?? [],
9126
+ ...buckets[".cjs"] ?? []
9127
+ ];
9128
+ if (jsFiles.length) {
9129
+ const { jsScanner: jsScanner2 } = await Promise.resolve().then(() => (init_javascript(), javascript_exports));
9130
+ tasks.push(scanInProcess(jsScanner2, jsFiles, root));
8756
9131
  }
8757
- log.blank();
8758
- log.info(`Doctor provider: ${report.provider}`);
8759
- for (const finding of report.findings) {
8760
- if (finding.level === "ok")
8761
- log.ok(finding.message);
8762
- else if (finding.level === "warning")
8763
- log.warn(finding.message);
8764
- else
8765
- log.error(finding.message);
9132
+ const tsFiles = [
9133
+ ...buckets[".ts"] ?? [],
9134
+ ...buckets[".tsx"] ?? []
9135
+ ];
9136
+ if (tsFiles.length) {
9137
+ const { tsScanner: tsScanner2 } = await Promise.resolve().then(() => (init_typescript(), typescript_exports));
9138
+ tasks.push(scanInProcess(tsScanner2, tsFiles, root));
8766
9139
  }
8767
- log.blank();
8768
- if (!report.ok) {
8769
- log.error(report.strict && report.errors === 0 ? `doctor failed in strict mode with ${report.warnings} warning(s)` : `doctor failed with ${report.errors} error(s)`);
8770
- process.exit(1);
9140
+ if (buckets[".vue"]?.length) {
9141
+ const { vueScanner: vueScanner2 } = await Promise.resolve().then(() => (init_vue(), vue_exports));
9142
+ tasks.push(scanInProcess(vueScanner2, buckets[".vue"], root));
9143
+ }
9144
+ for (const r of await Promise.all(tasks)) {
9145
+ result.entries.push(...r.entries);
9146
+ result.warnings.push(...r.warnings);
9147
+ }
9148
+ result.entries.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
9149
+ for (const e of result.entries) {
9150
+ if (e.total_lines > opts.maxLines) {
9151
+ result.warnings.push({
9152
+ path: e.path,
9153
+ message: `file exceeds --max-lines (${e.total_lines} > ${opts.maxLines})`
9154
+ });
9155
+ }
9156
+ }
9157
+ const yamlBody = renderYaml(result.entries, { generator: `cdd-kit ${_pkg.version}` });
9158
+ const totalSrc = result.entries.reduce((s, e) => s + e.total_lines, 0);
9159
+ const mapLines = yamlBody.split("\n").length;
9160
+ const compression = totalSrc === 0 ? 0 : totalSrc / mapLines;
9161
+ const summaryLine = `scanned ${result.entries.length} files, ${totalSrc} src lines -> ${opts.out} (${mapLines} lines, compression ${compression.toFixed(1)}x)`;
9162
+ for (const w of result.warnings) {
9163
+ log.warn(`${w.path}: ${w.message}`);
8771
9164
  }
8772
- if (report.warnings > 0) {
8773
- log.warn(`doctor completed with ${report.warnings} warning(s)`);
8774
- } else {
8775
- log.ok("doctor passed");
9165
+ if (opts.check) {
9166
+ const existing = existsSync16(opts.out) ? readFileSync17(opts.out, "utf8") : "";
9167
+ const normalize = (s) => s.replace(/^# generated: [^\n]+\n/m, "# generated: <normalized>\n");
9168
+ if (normalize(existing) !== normalize(yamlBody)) {
9169
+ log.error(`code-map out of date: ${opts.out} would change. Run \`cdd-kit code-map\` to regenerate.`);
9170
+ return 1;
9171
+ }
9172
+ log.ok(`code-map up to date: ${opts.out}`);
9173
+ return 0;
8776
9174
  }
8777
- log.blank();
9175
+ mkdirSync8(dirname5(opts.out), { recursive: true });
9176
+ writeFileSync9(opts.out, yamlBody, "utf8");
9177
+ log.ok(`${summaryLine} (${Date.now() - start}ms)`);
9178
+ return 0;
8778
9179
  }
8779
- var init_doctor = __esm({
8780
- "src/commands/doctor.ts"() {
9180
+ var _require, _pkgPath, _pkg;
9181
+ var init_code_map = __esm({
9182
+ "src/commands/code-map.ts"() {
8781
9183
  "use strict";
8782
9184
  init_logger();
8783
- init_provider();
8784
- init_freshness();
9185
+ init_yaml_writer();
9186
+ init_orchestrator();
9187
+ init_config();
9188
+ _require = createRequire(import.meta.url);
9189
+ _pkgPath = join19(fileURLToPath2(import.meta.url), "..", "..", "..", "package.json");
9190
+ _pkg = JSON.parse(readFileSync17(_pkgPath, "utf8"));
8785
9191
  }
8786
9192
  });
8787
9193
 
8788
- // src/commands/migrate.ts
8789
- var migrate_exports = {};
8790
- __export(migrate_exports, {
8791
- migrate: () => migrate
9194
+ // src/commands/refresh.ts
9195
+ var refresh_exports = {};
9196
+ __export(refresh_exports, {
9197
+ refresh: () => refresh
8792
9198
  });
8793
- import { join as join19 } from "path";
8794
- import { cpSync as cpSync2, existsSync as existsSync16, mkdirSync as mkdirSync7, readdirSync as readdirSync9, readFileSync as readFileSync17, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync8 } from "fs";
8795
- import yaml3 from "js-yaml";
8796
- function backupChangeDir(cwd, changeId, sessionStamp) {
8797
- const backupRoot = join19(cwd, ".cdd", "migrate-backup", sessionStamp);
8798
- const backupDir2 = join19(backupRoot, changeId);
8799
- mkdirSync7(backupRoot, { recursive: true });
8800
- const sourceDir = join19(cwd, "specs", "changes", changeId);
8801
- if (existsSync16(sourceDir)) {
8802
- cpSync2(sourceDir, backupDir2, { recursive: true });
9199
+ import { existsSync as existsSync17, mkdirSync as mkdirSync9, readdirSync as readdirSync10, copyFileSync as copyFileSync4, readFileSync as readFileSync18, writeFileSync as writeFileSync10 } from "fs";
9200
+ import { dirname as dirname6, join as join20, relative as relative5 } from "path";
9201
+ import { createHash as createHash4 } from "crypto";
9202
+ function fileHash2(filePath) {
9203
+ return createHash4("sha256").update(readFileSync18(filePath)).digest("hex");
9204
+ }
9205
+ function planForceRefresh(srcDir, destDir, sectionLabel) {
9206
+ const plan = [];
9207
+ if (!existsSync17(srcDir))
9208
+ return plan;
9209
+ function walk(curSrc, curDest) {
9210
+ let items;
9211
+ try {
9212
+ items = readdirSync10(curSrc, { withFileTypes: true });
9213
+ } catch {
9214
+ return;
9215
+ }
9216
+ for (const item of items) {
9217
+ const sp = join20(curSrc, item.name);
9218
+ const dp = join20(curDest, item.name);
9219
+ if (item.isDirectory()) {
9220
+ walk(sp, dp);
9221
+ continue;
9222
+ }
9223
+ if (!item.isFile())
9224
+ continue;
9225
+ const rel = join20(sectionLabel, relative5(srcDir, sp)).replace(/\\/g, "/");
9226
+ if (!existsSync17(dp)) {
9227
+ plan.push({ src: sp, dest: dp, rel, action: "add" });
9228
+ } else if (fileHash2(sp) !== fileHash2(dp)) {
9229
+ plan.push({ src: sp, dest: dp, rel, action: "overwrite" });
9230
+ } else {
9231
+ plan.push({ src: sp, dest: dp, rel, action: "skip" });
9232
+ }
9233
+ }
8803
9234
  }
8804
- return backupDir2;
9235
+ walk(srcDir, destDir);
9236
+ return plan;
8805
9237
  }
8806
- function buildLegacyContextManifest(changeId) {
8807
- return [
8808
- "# Context Manifest",
8809
- "",
8810
- "Generated by `cdd-kit migrate` for an existing change.",
8811
- "Legacy manifest. Forbidden paths come from `.cdd/context-policy.json`.",
8812
- "",
8813
- "## Affected Surfaces",
8814
- "- legacy-unknown",
8815
- "",
8816
- "## Allowed Paths",
8817
- `- specs/changes/${changeId}/`,
8818
- "",
8819
- "## Required Contracts",
8820
- "- legacy-unknown",
8821
- "",
8822
- "## Required Tests",
8823
- "- legacy-unknown",
8824
- "",
8825
- "## Agent Work Packets",
8826
- "",
8827
- "## Context Expansion Requests",
8828
- "-",
8829
- "",
8830
- "## Approved Expansions",
8831
- "-",
8832
- ""
8833
- ].join("\n");
9238
+ function planSingleFile(src, dest, rel) {
9239
+ if (!existsSync17(src))
9240
+ return null;
9241
+ if (!existsSync17(dest))
9242
+ return { src, dest, rel, action: "add" };
9243
+ if (fileHash2(src) !== fileHash2(dest))
9244
+ return { src, dest, rel, action: "overwrite" };
9245
+ return { src, dest, rel, action: "skip" };
9246
+ }
9247
+ function ensureGitignoreEntry2(cwd, entry) {
9248
+ const path = join20(cwd, ".gitignore");
9249
+ const trimmed = entry.trim();
9250
+ if (!trimmed)
9251
+ return false;
9252
+ const re = new RegExp(`^\\s*${trimmed.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\$&")}\\s*$`, "m");
9253
+ let existing = "";
9254
+ if (existsSync17(path))
9255
+ existing = readFileSync18(path, "utf8");
9256
+ if (re.test(existing))
9257
+ return false;
9258
+ const sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
9259
+ const block = existing.length === 0 ? `# cdd-kit generated backups (do not commit)
9260
+ ${trimmed}
9261
+ ` : `${sep}
9262
+ # cdd-kit generated backups (do not commit)
9263
+ ${trimmed}
9264
+ `;
9265
+ writeFileSync10(path, existing + block, "utf8");
9266
+ return true;
8834
9267
  }
8835
- function buildContextGovernedManifest(changeId) {
8836
- return [
8837
- "# Context Manifest",
8838
- "",
8839
- "Generated by `cdd-kit migrate --enable-context-governance` for an existing change.",
8840
- "Review and narrow the allowed paths before assigning implementation work.",
8841
- "Forbidden paths come from `.cdd/context-policy.json`.",
8842
- "",
8843
- "## Affected Surfaces",
8844
- "- legacy-unknown",
8845
- "",
8846
- "## Allowed Paths",
8847
- `- specs/changes/${changeId}/`,
8848
- "- specs/context/project-map.md",
8849
- "- specs/context/contracts-index.md",
8850
- "",
8851
- "## Required Contracts",
8852
- "- legacy-unknown",
8853
- "",
8854
- "## Required Tests",
8855
- "- legacy-unknown",
8856
- "",
8857
- "## Agent Work Packets",
8858
- "",
8859
- "### change-classifier",
8860
- "- allowed:",
8861
- ` - specs/changes/${changeId}/`,
8862
- " - specs/context/project-map.md",
8863
- " - specs/context/contracts-index.md",
8864
- "",
8865
- "## Context Expansion Requests",
8866
- "",
8867
- "<!--",
8868
- "Agents must request context expansion instead of reading outside their work packet.",
8869
- "Use this format only for real requests:",
8870
- "",
8871
- "- request-id: CER-001",
8872
- " requested_paths:",
8873
- " - src/example.ts",
8874
- " reason: why this file is required",
8875
- " status: pending",
8876
- "-->",
8877
- "",
8878
- "## Approved Expansions",
8879
- "-",
8880
- ""
8881
- ].join("\n");
9268
+ function applyPlan(plan, backupRoot) {
9269
+ let added = 0;
9270
+ let overwritten = 0;
9271
+ for (const item of plan) {
9272
+ if (item.action === "skip")
9273
+ continue;
9274
+ if (item.action === "overwrite") {
9275
+ const backupPath = join20(backupRoot, item.rel);
9276
+ mkdirSync9(dirname6(backupPath), { recursive: true });
9277
+ copyFileSync4(item.dest, backupPath);
9278
+ overwritten += 1;
9279
+ } else {
9280
+ added += 1;
9281
+ }
9282
+ mkdirSync9(dirname6(item.dest), { recursive: true });
9283
+ copyFileSync4(item.src, item.dest);
9284
+ }
9285
+ return { added, overwritten };
8882
9286
  }
8883
- function parseLegacyFrontmatter(content) {
9287
+ function parseAgentFrontmatter(content) {
8884
9288
  const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
8885
9289
  if (!m)
8886
9290
  return {};
8887
- const out = {};
9291
+ const fm = {};
8888
9292
  for (const line of m[1].split(/\r?\n/)) {
8889
- const colon = line.indexOf(":");
8890
- if (colon === -1)
8891
- continue;
8892
- const key = line.slice(0, colon).trim();
8893
- if (!key)
8894
- continue;
8895
- out[key] = line.slice(colon + 1).trim();
8896
- }
8897
- return out;
8898
- }
8899
- function parseListField(raw) {
8900
- if (!raw)
8901
- return [];
8902
- const trimmed = raw.trim();
8903
- if (!trimmed || trimmed === "[]")
8904
- return [];
8905
- const inner = trimmed.startsWith("[") && trimmed.endsWith("]") ? trimmed.slice(1, -1) : trimmed;
8906
- return inner.split(",").map((item) => item.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
8907
- }
8908
- function parseLegacyTaskList(body) {
8909
- const lines = body.split(/\r?\n/);
8910
- const rows = [];
8911
- let currentSection;
8912
- for (const raw of lines) {
8913
- const headerMatch = raw.match(/^##\s+\d+\.\s+(.*)\s*$/);
8914
- if (headerMatch) {
8915
- currentSection = headerMatch[1].trim();
9293
+ const km = line.match(/^([a-zA-Z_-]+):\s*(.+?)\s*$/);
9294
+ if (!km)
8916
9295
  continue;
9296
+ if (km[1] === "name")
9297
+ fm.name = km[2].trim();
9298
+ if (km[1] === "model")
9299
+ fm.model = km[2].trim();
9300
+ }
9301
+ return fm;
9302
+ }
9303
+ function resyncModelPolicy(cwd) {
9304
+ const policyPath = join20(cwd, ".cdd", "model-policy.json");
9305
+ const result = { changed: false, diff: [], policyPath };
9306
+ if (!existsSync17(AGENTS_HOME))
9307
+ return result;
9308
+ const desired = {};
9309
+ const agentFiles = readdirSync10(AGENTS_HOME, { withFileTypes: true }).filter((d) => d.isFile() && d.name.endsWith(".md"));
9310
+ for (const f of agentFiles) {
9311
+ const content = readFileSync18(join20(AGENTS_HOME, f.name), "utf8");
9312
+ const fm = parseAgentFrontmatter(content);
9313
+ if (fm.name && fm.model)
9314
+ desired[fm.name] = fm.model;
9315
+ }
9316
+ if (Object.keys(desired).length === 0)
9317
+ return result;
9318
+ let existing = {};
9319
+ if (existsSync17(policyPath)) {
9320
+ try {
9321
+ existing = JSON.parse(readFileSync18(policyPath, "utf8"));
9322
+ } catch {
8917
9323
  }
8918
- const itemMatch = raw.match(/^\s*-\s*\[([ xX\-])\]\s+(\d+(?:\.\d+)*)\s+(.*)\s*$/);
8919
- if (!itemMatch)
8920
- continue;
8921
- const mark = itemMatch[1];
8922
- const id = itemMatch[2];
8923
- const title = itemMatch[3].trim();
8924
- let status = "pending";
8925
- if (mark === "x" || mark === "X")
8926
- status = "done";
8927
- else if (mark === "-")
8928
- status = "skipped";
8929
- rows.push({ id, title, status, section: currentSection });
8930
9324
  }
8931
- return rows;
8932
- }
8933
- function migrateTasksFile(changeId, changeDir, enableContextGovernance, detectedTier, changed, warnings, pendingWrites, pendingDeletes) {
8934
- const newPath = join19(changeDir, "tasks.yml");
8935
- const legacyPath = join19(changeDir, "tasks.md");
8936
- if (existsSync16(newPath)) {
8937
- return;
9325
+ const existingRoles = existing.roles && typeof existing.roles === "object" ? existing.roles : {};
9326
+ for (const [agent, model] of Object.entries(desired)) {
9327
+ if (existingRoles[agent] !== model) {
9328
+ result.diff.push({ agent, from: existingRoles[agent] ?? null, to: model });
9329
+ }
8938
9330
  }
8939
- if (!existsSync16(legacyPath)) {
8940
- warnings.push("tasks.md not found and tasks.yml missing \u2014 skipping tasks migration");
8941
- return;
9331
+ if (result.diff.length === 0)
9332
+ return result;
9333
+ const merged = {
9334
+ ...existing,
9335
+ roles: desired
9336
+ };
9337
+ if (!("schema-version" in merged))
9338
+ merged["schema-version"] = "0.2.0";
9339
+ if (!("provider" in merged))
9340
+ merged["provider"] = "claude";
9341
+ mkdirSync9(dirname6(policyPath), { recursive: true });
9342
+ writeFileSync10(policyPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
9343
+ result.changed = true;
9344
+ return result;
9345
+ }
9346
+ function planTemplateRefresh(cwd) {
9347
+ const sections = [];
9348
+ sections.push({
9349
+ name: "specs/templates",
9350
+ plan: planForceRefresh(ASSET.specsTemplates, join20(cwd, "specs", "templates"), "specs/templates")
9351
+ });
9352
+ sections.push({
9353
+ name: "tests/templates",
9354
+ plan: planForceRefresh(ASSET.testsTemplates, join20(cwd, "tests", "templates"), "tests/templates")
9355
+ });
9356
+ const ciTemplatesAsset = join20(ASSET.ci, "..", "ci-templates");
9357
+ if (existsSync17(ciTemplatesAsset)) {
9358
+ sections.push({
9359
+ name: "ci-templates",
9360
+ plan: planForceRefresh(ciTemplatesAsset, join20(cwd, "ci-templates"), "ci-templates")
9361
+ });
8942
9362
  }
8943
- const raw = readFileSync17(legacyPath, "utf8");
8944
- const fm = parseLegacyFrontmatter(raw);
8945
- const bodyMatch = raw.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?([\s\S]*)$/);
8946
- const body = bodyMatch ? bodyMatch[1] : raw;
8947
- const tasksRows = parseLegacyTaskList(body);
8948
- const data = {};
8949
- data["change-id"] = fm["change-id"] || changeId;
8950
- data["status"] = fm["status"] || "in-progress";
8951
- if (fm["tier"] && /^\d+$/.test(fm["tier"])) {
8952
- data["tier"] = parseInt(fm["tier"], 10);
8953
- } else if (detectedTier) {
8954
- data["tier"] = parseInt(detectedTier, 10);
9363
+ const wfAsset = join20(ASSET.githubWorkflows, "contract-driven-gates.yml");
9364
+ const wfDest = join20(cwd, ".github", "workflows", "contract-driven-gates.yml");
9365
+ const wfPlan = planSingleFile(wfAsset, wfDest, ".github/workflows/contract-driven-gates.yml");
9366
+ if (wfPlan)
9367
+ sections.push({ name: ".github/workflows/contract-driven-gates.yml", plan: [wfPlan] });
9368
+ const total = sections.flatMap((s) => s.plan);
9369
+ return { sections, total };
9370
+ }
9371
+ function summarizePlan(items) {
9372
+ return {
9373
+ add: items.filter((i) => i.action === "add").length,
9374
+ overwrite: items.filter((i) => i.action === "overwrite").length,
9375
+ skip: items.filter((i) => i.action === "skip").length
9376
+ };
9377
+ }
9378
+ async function refresh(opts) {
9379
+ const cwd = process.cwd();
9380
+ const apply = !!opts.yes;
9381
+ log.blank();
9382
+ log.info(apply ? "cdd-kit refresh \u2014 applying changes" : "cdd-kit refresh \u2014 dry-run preview (re-run with --yes to apply)");
9383
+ log.blank();
9384
+ if (!opts.noUpdate) {
9385
+ log.info("[1/6] ~/.claude/agents and ~/.claude/skills (via cdd-kit update)");
9386
+ await update({ yes: apply, provider: opts.provider ?? "auto" });
8955
9387
  } else {
8956
- data["tier"] = null;
9388
+ log.dim("[1/6] skipped (--no-update)");
8957
9389
  }
8958
- if (enableContextGovernance || fm["context-governance"] === "v1") {
8959
- data["context-governance"] = "v1";
9390
+ log.blank();
9391
+ if (!opts.noUpgrade) {
9392
+ log.info("[2/6] add missing project files (via cdd-kit upgrade)");
9393
+ await upgrade({ yes: apply, provider: opts.provider ?? "auto" });
9394
+ } else {
9395
+ log.dim("[2/6] skipped (--no-upgrade)");
8960
9396
  }
8961
- const archive2 = parseListField(fm["archive-tasks"]);
8962
- data["archive-tasks"] = archive2.length > 0 ? archive2 : ["7.1", "7.2"];
8963
- const deps = parseListField(fm["depends-on"]);
8964
- data["depends-on"] = deps;
8965
- data["tasks"] = tasksRows.map((r) => {
8966
- const out = { id: r.id, title: r.title, status: r.status };
8967
- if (r.section)
8968
- out["section"] = r.section;
8969
- return out;
8970
- });
8971
- const yamlOut = yaml3.dump(data, { lineWidth: -1, noRefs: true });
8972
- pendingWrites.push({ path: newPath, content: yamlOut });
8973
- pendingDeletes.push({ path: legacyPath });
8974
- changed.push(`tasks.md -> tasks.yml (${tasksRows.length} task(s) migrated)`);
8975
- }
8976
- function parseLegacyAgentLog(content) {
8977
- const lines = content.split(/\r?\n/);
8978
- const data = {};
8979
- let i = 0;
8980
- const topFieldRe = /^[ ]{0,1}-\s*([\w-]+):\s*(.*)$/;
8981
- while (i < lines.length) {
8982
- const line = lines[i];
8983
- const fieldMatch = line.match(topFieldRe);
8984
- if (!fieldMatch) {
8985
- i++;
8986
- continue;
8987
- }
8988
- const key = fieldMatch[1];
8989
- const inline = fieldMatch[2].trim();
8990
- if (key === "files-read" || key === "artifacts") {
8991
- const items = [];
8992
- let j = i + 1;
8993
- while (j < lines.length) {
8994
- const sub = lines[j];
8995
- if (topFieldRe.test(sub))
8996
- break;
8997
- if (/^#/.test(sub))
8998
- break;
8999
- const itemMatch = sub.match(/^\s{2,}-\s+(.+?)\s*$/);
9000
- if (itemMatch) {
9001
- items.push(itemMatch[1].trim());
9397
+ log.blank();
9398
+ let templateAdded = 0;
9399
+ let templateOverwritten = 0;
9400
+ let backupRoot = null;
9401
+ if (!opts.noTemplates) {
9402
+ log.info("[3/6] force-refresh kit-shipped templates");
9403
+ const { sections, total } = planTemplateRefresh(cwd);
9404
+ const summary = summarizePlan(total);
9405
+ if (summary.add === 0 && summary.overwrite === 0) {
9406
+ log.ok(" templates already up to date");
9407
+ } else {
9408
+ for (const s of sections) {
9409
+ const ss = summarizePlan(s.plan);
9410
+ if (ss.add === 0 && ss.overwrite === 0)
9411
+ continue;
9412
+ log.info(` ${s.name}: +${ss.add} ~${ss.overwrite} =${ss.skip}`);
9413
+ for (const item of s.plan.filter((i) => i.action !== "skip")) {
9414
+ log.dim(` ${item.action === "add" ? "+" : "~"} ${item.rel}`);
9415
+ }
9416
+ }
9417
+ if (apply) {
9418
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
9419
+ backupRoot = join20(cwd, ".cdd", ".refresh-backup", ts);
9420
+ const result = applyPlan(total, backupRoot);
9421
+ templateAdded = result.added;
9422
+ templateOverwritten = result.overwritten;
9423
+ log.ok(` applied: +${templateAdded} added, ~${templateOverwritten} overwritten`);
9424
+ if (templateOverwritten > 0) {
9425
+ log.info(` backup saved to: ${relative5(cwd, backupRoot).replace(/\\/g, "/")}`);
9426
+ if (ensureGitignoreEntry2(cwd, ".cdd/.refresh-backup/")) {
9427
+ log.info(" added `.cdd/.refresh-backup/` to .gitignore");
9428
+ }
9002
9429
  }
9003
- j++;
9004
- }
9005
- if (key === "files-read") {
9006
- data["files-read"] = items;
9007
9430
  } else {
9008
- data["artifacts"] = items.map((s) => {
9009
- const idx = s.indexOf(":");
9010
- if (idx === -1) {
9011
- return { type: "note", pointer: s };
9012
- }
9013
- const type = s.slice(0, idx).trim();
9014
- const pointer = s.slice(idx + 1).trim();
9015
- return { type, pointer };
9016
- });
9431
+ log.dim(" (dry-run \u2014 no changes written)");
9017
9432
  }
9018
- i = j;
9019
- continue;
9020
9433
  }
9021
- data[key] = inline;
9022
- i++;
9023
- }
9024
- return data;
9025
- }
9026
- function migrateAgentLogs(changeDir, changed, pendingWrites, pendingDeletes) {
9027
- const agentLogDir = join19(changeDir, "agent-log");
9028
- if (!existsSync16(agentLogDir))
9029
- return;
9030
- const mdLogs = readdirSync9(agentLogDir).filter((f) => f.endsWith(".md"));
9031
- for (const f of mdLogs) {
9032
- const fullPath = join19(agentLogDir, f);
9033
- const yamlName = f.replace(/\.md$/, ".yml");
9034
- const yamlFull = join19(agentLogDir, yamlName);
9035
- if (existsSync16(yamlFull))
9036
- continue;
9037
- const raw = readFileSync17(fullPath, "utf8");
9038
- const parsed = parseLegacyAgentLog(raw);
9039
- const yamlOut = yaml3.dump(parsed, { lineWidth: -1, noRefs: true });
9040
- pendingWrites.push({ path: yamlFull, content: yamlOut });
9041
- pendingDeletes.push({ path: fullPath });
9042
- changed.push(`agent-log/${f} -> agent-log/${yamlName}`);
9434
+ } else {
9435
+ log.dim("[3/6] skipped (--no-templates)");
9043
9436
  }
9044
- }
9045
- function migrateOne(changeId, changeDir, enableContextGovernance) {
9046
- const changed = [];
9047
- const warnings = [];
9048
- const pending = [];
9049
- const deletes = [];
9050
- let detectedTier = null;
9051
- const classifPath = join19(changeDir, "change-classification.md");
9052
- if (existsSync16(classifPath)) {
9053
- const content = readFileSync17(classifPath, "utf8");
9054
- const hasNewTierFormat = /^## Tier\s*\n\s*-\s*\d\s*$/m.test(content);
9055
- const oldMatch = content.match(/\*\*Tier[:\*]+\s*(?:Tier\s*)?(\d)/i) ?? content.match(/^-?\s*Tier:\s*(?:Tier\s*)?(\d)/mi);
9056
- if (oldMatch)
9057
- detectedTier = oldMatch[1];
9058
- if (!hasNewTierFormat) {
9059
- if (detectedTier) {
9060
- const addition = `
9061
- ## Tier
9062
- - ${detectedTier}
9063
- `;
9064
- if (!content.includes("\n## Tier\n")) {
9065
- changed.push(
9066
- `change-classification.md: appended "## Tier\\n- ${detectedTier}" (converted from old format)`
9067
- );
9068
- pending.push({ path: classifPath, content: content + addition });
9437
+ log.blank();
9438
+ if (!opts.noHooks) {
9439
+ const markerPath = join20(cwd, HOOKS_MARKER_PATH);
9440
+ if (existsSync17(markerPath)) {
9441
+ log.info("[4/6] re-install code-map pre-commit hook (marker found)");
9442
+ if (apply) {
9443
+ try {
9444
+ await installCodeMapHook(cwd);
9445
+ } catch (err) {
9446
+ log.warn(` hook re-install skipped: ${err.message}`);
9069
9447
  }
9070
9448
  } else {
9071
- warnings.push(
9072
- "change-classification.md: could not detect tier (no **Tier:** N or ## Tier N found). Set `tier: <0-5>` in tasks.yml frontmatter to enable tier-based gate checks."
9073
- );
9449
+ log.dim(" (dry-run \u2014 would re-install)");
9074
9450
  }
9075
9451
  } else {
9076
- const structured = content.match(/^## Tier\s*\n\s*-\s*(\d)\s*$/m);
9077
- if (structured)
9078
- detectedTier = structured[1];
9452
+ log.dim("[4/6] no .cdd/.hooks-installed marker \u2014 skipping (run `cdd-kit init --hooks` to install)");
9079
9453
  }
9454
+ } else {
9455
+ log.dim("[4/6] skipped (--no-hooks)");
9080
9456
  }
9081
- migrateTasksFile(changeId, changeDir, enableContextGovernance, detectedTier, changed, warnings, pending, deletes);
9082
- migrateAgentLogs(changeDir, changed, pending, deletes);
9083
- const manifestPath = join19(changeDir, "context-manifest.md");
9084
- if (!existsSync16(manifestPath)) {
9085
- changed.push(enableContextGovernance ? "context-manifest.md: added context-governance v1 manifest scaffold" : "context-manifest.md: added legacy context manifest scaffold");
9086
- pending.push({
9087
- path: manifestPath,
9088
- content: enableContextGovernance ? buildContextGovernedManifest(changeId) : buildLegacyContextManifest(changeId)
9089
- });
9090
- } else if (enableContextGovernance) {
9091
- warnings.push("context-manifest.md already exists \u2014 review allowed paths before relying on context-governance: v1");
9092
- }
9093
- return { result: { changed, warnings }, pending, deletes };
9094
- }
9095
- function commitWritesAtomically(pending, deletes) {
9096
- const renames = [];
9097
- try {
9098
- for (const write of pending) {
9099
- const tmp = `${write.path}.cdd-migrate.tmp`;
9100
- writeFileSync8(tmp, write.content, "utf8");
9101
- renames.push({ tmp, final: write.path });
9457
+ log.blank();
9458
+ log.info("[5/6] resync .cdd/model-policy.json roles from agent frontmatter");
9459
+ if (apply) {
9460
+ const r = resyncModelPolicy(cwd);
9461
+ if (r.diff.length === 0) {
9462
+ log.ok(" roles already match agent prompts");
9463
+ } else {
9464
+ for (const d of r.diff) {
9465
+ log.info(` ${d.agent}: ${d.from ?? "(absent)"} \u2192 ${d.to}`);
9466
+ }
9467
+ log.ok(` ${r.diff.length} role binding(s) updated`);
9102
9468
  }
9103
- } catch (err) {
9104
- for (const r of renames) {
9469
+ } else {
9470
+ const fakeApply = () => {
9471
+ };
9472
+ fakeApply();
9473
+ log.dim(" (dry-run \u2014 drift will be reported only when applied)");
9474
+ }
9475
+ log.blank();
9476
+ if (!opts.noCodeMap) {
9477
+ log.info("[6/6] regenerate .cdd/code-map.yml");
9478
+ if (apply) {
9105
9479
  try {
9106
- rmSync2(r.tmp, { force: true });
9107
- } catch {
9480
+ await codeMap({
9481
+ path: ".",
9482
+ out: ".cdd/code-map.yml",
9483
+ include: [],
9484
+ exclude: [],
9485
+ check: false,
9486
+ maxLines: 600
9487
+ });
9488
+ } catch (err) {
9489
+ log.warn(` code-map skipped: ${err.message}`);
9108
9490
  }
9491
+ } else {
9492
+ log.dim(" (dry-run \u2014 would regenerate)");
9109
9493
  }
9110
- throw err;
9494
+ } else {
9495
+ log.dim("[6/6] skipped (--no-code-map)");
9111
9496
  }
9112
- for (const r of renames) {
9113
- renameSync(r.tmp, r.final);
9497
+ log.blank();
9498
+ if (apply) {
9499
+ log.ok("refresh complete.");
9500
+ if (backupRoot) {
9501
+ log.info(`Backup of overwritten templates: ${relative5(cwd, backupRoot).replace(/\\/g, "/")}`);
9502
+ }
9503
+ log.info("Next: review changes with `git diff`, then commit:");
9504
+ log.dim(" git add .cdd/code-map.yml .cdd/model-policy.json specs/templates tests/templates ci-templates .github/workflows");
9505
+ log.dim(' git commit -m "chore: cdd-kit refresh"');
9506
+ } else {
9507
+ log.info("Dry-run finished. Re-run with `--yes` to apply.");
9114
9508
  }
9115
- for (const d of deletes) {
9116
- try {
9117
- rmSync2(d.path, { force: true });
9118
- } catch {
9119
- }
9509
+ log.blank();
9510
+ }
9511
+ var HOOKS_MARKER_PATH;
9512
+ var init_refresh = __esm({
9513
+ "src/commands/refresh.ts"() {
9514
+ "use strict";
9515
+ init_paths();
9516
+ init_logger();
9517
+ init_update();
9518
+ init_upgrade();
9519
+ init_code_map();
9520
+ init_code_map_hook();
9521
+ HOOKS_MARKER_PATH = ".cdd/.hooks-installed";
9120
9522
  }
9523
+ });
9524
+
9525
+ // src/commands/doctor.ts
9526
+ var doctor_exports = {};
9527
+ __export(doctor_exports, {
9528
+ doctor: () => doctor
9529
+ });
9530
+ import { existsSync as existsSync18, readdirSync as readdirSync11, readFileSync as readFileSync19 } from "fs";
9531
+ import { createHash as createHash5 } from "crypto";
9532
+ import { join as join21 } from "path";
9533
+ function fileExists(cwd, relPath) {
9534
+ return existsSync18(join21(cwd, relPath));
9121
9535
  }
9122
- async function migrate(changeId, opts = {}) {
9123
- const cwd = process.cwd();
9124
- const dryRun = opts.dryRun ?? false;
9125
- const enableContextGovernance = opts.enableContextGovernance ?? false;
9126
- const noBackup = opts.noBackup ?? false;
9127
- const idsToMigrate = [];
9128
- if (opts.all) {
9129
- const changesDir = join19(cwd, "specs", "changes");
9130
- if (!existsSync16(changesDir)) {
9131
- log.info("No specs/changes/ directory found \u2014 nothing to migrate.");
9132
- return;
9133
- }
9134
- idsToMigrate.push(
9135
- ...readdirSync9(changesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name)
9136
- );
9137
- } else if (changeId) {
9138
- const specificDir = join19(cwd, "specs", "changes", changeId);
9139
- if (!existsSync16(specificDir)) {
9140
- log.error(`Change not found: specs/changes/${changeId}`);
9141
- process.exit(1);
9142
- }
9143
- idsToMigrate.push(changeId);
9144
- } else {
9145
- log.error("Usage: cdd-kit migrate <change-id> | cdd-kit migrate --all [--dry-run] [--no-backup]");
9146
- process.exit(1);
9536
+ function findFiles(dir, predicate, found = []) {
9537
+ if (!existsSync18(dir))
9538
+ return found;
9539
+ for (const entry of readdirSync11(dir, { withFileTypes: true })) {
9540
+ const fullPath = join21(dir, entry.name);
9541
+ if (entry.isDirectory())
9542
+ findFiles(fullPath, predicate, found);
9543
+ else if (entry.isFile() && predicate(entry.name))
9544
+ found.push(fullPath);
9147
9545
  }
9148
- if (idsToMigrate.length === 0) {
9149
- log.info("No changes found to migrate.");
9150
- return;
9546
+ return found;
9547
+ }
9548
+ function sha256OfFile3(path) {
9549
+ try {
9550
+ return createHash5("sha256").update(readFileSync19(path)).digest("hex");
9551
+ } catch {
9552
+ return "";
9151
9553
  }
9152
- if (dryRun) {
9153
- log.info("Dry run \u2014 no files will be written.");
9154
- log.blank();
9554
+ }
9555
+ function inputDigest(paths) {
9556
+ const combined = paths.slice().sort().map((p) => `${p}:${sha256OfFile3(p)}`).join("\n");
9557
+ return createHash5("sha256").update(combined).digest("hex");
9558
+ }
9559
+ function readContextIndexMetadata(filePath) {
9560
+ if (!existsSync18(filePath))
9561
+ return {};
9562
+ const text = readFileSync19(filePath, "utf8");
9563
+ const out = {};
9564
+ const digestMatch = text.match(/^inputs-digest:\s*([a-f0-9]+)/m);
9565
+ if (digestMatch)
9566
+ out.inputsDigest = digestMatch[1];
9567
+ const missingMatch = text.match(/^missing-summary-count:\s*(\d+)/m);
9568
+ if (missingMatch)
9569
+ out.missingSummary = Number(missingMatch[1]);
9570
+ return out;
9571
+ }
9572
+ function checkContextFreshness(cwd) {
9573
+ const findings = [];
9574
+ const projectMap = join21(cwd, "specs", "context", "project-map.md");
9575
+ const contractsIndex = join21(cwd, "specs", "context", "contracts-index.md");
9576
+ const contextPolicy = join21(cwd, ".cdd", "context-policy.json");
9577
+ const contractFiles = findFiles(
9578
+ join21(cwd, "contracts"),
9579
+ (name) => name.endsWith(".md") && name !== "INDEX.md" && name !== "CHANGELOG.md"
9580
+ );
9581
+ if (!existsSync18(projectMap) || !existsSync18(contractsIndex)) {
9582
+ findings.push({
9583
+ level: "warning",
9584
+ message: "specs/context indexes are missing; run cdd-kit context-scan before classification"
9585
+ });
9586
+ return findings;
9155
9587
  }
9156
- const sessionStamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
9157
- let migratedCount = 0;
9158
- let upToDateCount = 0;
9159
- const backupRoot = join19(cwd, ".cdd", "migrate-backup", sessionStamp);
9160
- for (const id of idsToMigrate) {
9161
- const changeDir = join19(cwd, "specs", "changes", id);
9162
- if (!existsSync16(changeDir)) {
9163
- log.warn(` ${id}: directory not found \u2014 skipping`);
9164
- continue;
9165
- }
9166
- const { result, pending, deletes } = migrateOne(id, changeDir, enableContextGovernance);
9167
- const { changed, warnings } = result;
9168
- if (changed.length === 0) {
9169
- log.info(` ${id}: already up to date`);
9170
- upToDateCount++;
9171
- for (const w of warnings)
9172
- log.warn(` ${id}: ${w}`);
9173
- continue;
9174
- }
9175
- if (!dryRun) {
9176
- try {
9177
- if (!noBackup)
9178
- backupChangeDir(cwd, id, sessionStamp);
9179
- commitWritesAtomically(pending, deletes);
9180
- } catch (err) {
9181
- log.error(` ${id}: migration failed \u2014 ${err.message}`);
9182
- if (!noBackup) {
9183
- log.error(` ${id}: restore from .cdd/migrate-backup/${sessionStamp}/${id}/`);
9184
- }
9185
- process.exit(1);
9588
+ const projectMapMeta = readContextIndexMetadata(projectMap);
9589
+ const contractsIndexMeta = readContextIndexMetadata(contractsIndex);
9590
+ const projectInputDigest = inputDigest([contextPolicy].filter(existsSync18));
9591
+ if (projectMapMeta.inputsDigest === void 0) {
9592
+ findings.push({
9593
+ level: "warning",
9594
+ message: "specs/context/project-map.md was generated by an older cdd-kit (no inputs-digest); re-run cdd-kit context-scan"
9595
+ });
9596
+ } else if (projectInputDigest && projectMapMeta.inputsDigest !== projectInputDigest) {
9597
+ findings.push({
9598
+ level: "warning",
9599
+ message: "specs/context/project-map.md inputs changed (.cdd/context-policy.json); re-run cdd-kit context-scan"
9600
+ });
9601
+ }
9602
+ const contractsInputDigest = inputDigest(contractFiles);
9603
+ if (contractsIndexMeta.inputsDigest === void 0) {
9604
+ findings.push({
9605
+ level: "warning",
9606
+ message: "specs/context/contracts-index.md was generated by an older cdd-kit (no inputs-digest); re-run cdd-kit context-scan"
9607
+ });
9608
+ } else if (contractsInputDigest && contractsIndexMeta.inputsDigest !== contractsInputDigest) {
9609
+ findings.push({
9610
+ level: "warning",
9611
+ message: "specs/context/contracts-index.md inputs changed (contracts/*); re-run cdd-kit context-scan"
9612
+ });
9613
+ }
9614
+ if (contractsIndexMeta.missingSummary !== void 0 && contractsIndexMeta.missingSummary > 0) {
9615
+ findings.push({
9616
+ level: "warning",
9617
+ message: `contracts-index reports ${contractsIndexMeta.missingSummary} contract(s) without deterministic summary metadata`
9618
+ });
9619
+ }
9620
+ if (findings.length === 0) {
9621
+ findings.push({ level: "ok", message: "context indexes are present and fresh" });
9622
+ }
9623
+ return findings;
9624
+ }
9625
+ function readAgentModel(path) {
9626
+ try {
9627
+ const text = readFileSync19(path, "utf8");
9628
+ const m = text.match(/^model:\s*(\S+)/m);
9629
+ return m ? m[1] : null;
9630
+ } catch {
9631
+ return null;
9632
+ }
9633
+ }
9634
+ function checkModelPolicyDrift(cwd) {
9635
+ const policyPath = join21(cwd, ".cdd", "model-policy.json");
9636
+ if (!existsSync18(policyPath))
9637
+ return [];
9638
+ let policy;
9639
+ try {
9640
+ policy = JSON.parse(readFileSync19(policyPath, "utf8"));
9641
+ } catch {
9642
+ return [{ level: "warning", message: ".cdd/model-policy.json is not valid JSON" }];
9643
+ }
9644
+ const roles = policy.roles ?? {};
9645
+ if (Object.keys(roles).length === 0) {
9646
+ return [{
9647
+ level: "warning",
9648
+ message: ".cdd/model-policy.json has no role bindings; run cdd-kit upgrade to install defaults"
9649
+ }];
9650
+ }
9651
+ const candidateDirs = [
9652
+ join21(cwd, ".claude", "agents"),
9653
+ process.env.HOME ? join21(process.env.HOME, ".claude", "agents") : "",
9654
+ process.env.USERPROFILE ? join21(process.env.USERPROFILE, ".claude", "agents") : ""
9655
+ ].filter((p) => p && existsSync18(p));
9656
+ if (candidateDirs.length === 0)
9657
+ return [];
9658
+ const findings = [];
9659
+ for (const [role, expected] of Object.entries(roles)) {
9660
+ let foundAny = false;
9661
+ for (const dir of candidateDirs) {
9662
+ const path = join21(dir, `${role}.md`);
9663
+ if (!existsSync18(path))
9664
+ continue;
9665
+ foundAny = true;
9666
+ const actual = readAgentModel(path);
9667
+ if (actual && actual !== expected) {
9668
+ findings.push({
9669
+ level: "warning",
9670
+ message: `model-policy drift: ${role} expected ${expected}, agent prompt uses ${actual} (${path})`
9671
+ });
9186
9672
  }
9187
9673
  }
9188
- log.ok(` ${id}: migrated`);
9189
- for (const c of changed)
9190
- log.info(` + ${c}`);
9191
- migratedCount++;
9192
- for (const w of warnings)
9193
- log.warn(` ${id}: ${w}`);
9194
- }
9195
- log.blank();
9196
- if (dryRun) {
9197
- log.info(`Dry run complete: ${migratedCount} change(s) would be updated, ${upToDateCount} already up to date.`);
9198
- } else {
9199
- log.ok(`Migration complete: ${migratedCount} updated, ${upToDateCount} already up to date.`);
9200
- if (migratedCount > 0 && !noBackup) {
9201
- log.info(`Backup: ${backupRoot}`);
9202
- log.info('Next: git add specs/changes/ && git commit -m "chore: migrate changes to YAML format"');
9203
- log.info("When stable, remove backup: rm -rf .cdd/migrate-backup/");
9674
+ if (!foundAny) {
9204
9675
  }
9205
9676
  }
9206
- }
9207
- var init_migrate = __esm({
9208
- "src/commands/migrate.ts"() {
9209
- "use strict";
9210
- init_logger();
9677
+ if (findings.length === 0) {
9678
+ findings.push({ level: "ok", message: "model-policy roles match installed agent prompts" });
9211
9679
  }
9212
- });
9213
-
9214
- // src/commands/upgrade.ts
9215
- var upgrade_exports = {};
9216
- __export(upgrade_exports, {
9217
- upgrade: () => upgrade
9218
- });
9219
- import { existsSync as existsSync17, mkdirSync as mkdirSync8, readdirSync as readdirSync10, copyFileSync as copyFileSync3, readFileSync as readFileSync18, writeFileSync as writeFileSync9 } from "fs";
9220
- import { dirname as dirname5, join as join20, relative as relative4 } from "path";
9221
- function planMissingFiles(srcDir, destDir, label, planned) {
9222
- if (!existsSync17(srcDir))
9223
- return;
9224
- for (const entry of readdirSync10(srcDir, { withFileTypes: true })) {
9225
- const src = join20(srcDir, entry.name);
9226
- const dest = join20(destDir, entry.name);
9227
- if (entry.isDirectory()) {
9228
- planMissingFiles(src, dest, join20(label, entry.name), planned);
9229
- continue;
9680
+ return findings;
9681
+ }
9682
+ async function checkAgentLint(cwd) {
9683
+ const agentsDir = join21(cwd, ".claude", "agents");
9684
+ if (!existsSync18(agentsDir))
9685
+ return [];
9686
+ try {
9687
+ const { readdirSync: rds, readFileSync: rfs } = await import("fs");
9688
+ const files = rds(agentsDir).filter((f) => f.endsWith(".md"));
9689
+ if (files.length === 0)
9690
+ return [];
9691
+ const findings = [];
9692
+ for (const filename of files) {
9693
+ const content = rfs(join21(agentsDir, filename), "utf8");
9694
+ const artifactsSection = content.match(
9695
+ /### Required artifacts for this agent\s*\n[\s\S]*?(?=\n#{2,3} |\n---|\s*$)/
9696
+ )?.[0];
9697
+ if (!artifactsSection || !/```ya?ml\s*\nartifacts:/.test(artifactsSection)) {
9698
+ findings.push({
9699
+ level: "warning",
9700
+ message: `lint-agents: ${filename}: missing artifacts YAML block in Required artifacts section`
9701
+ });
9702
+ }
9703
+ const readScopeCount = (content.match(/^## Read scope\s*$/gm) ?? []).length;
9704
+ if (readScopeCount > 1) {
9705
+ findings.push({
9706
+ level: "warning",
9707
+ message: `lint-agents: ${filename}: duplicate ## Read scope headings (${readScopeCount})`
9708
+ });
9709
+ }
9230
9710
  }
9231
- if (!existsSync17(dest)) {
9232
- planned.push({ src, dest, rel: join20(label, relative4(srcDir, src)) });
9711
+ if (findings.length === 0) {
9712
+ findings.push({ level: "ok", message: "lint-agents: all agent prompts pass shape checks" });
9233
9713
  }
9714
+ return findings;
9715
+ } catch {
9716
+ return [];
9234
9717
  }
9235
9718
  }
9236
- function planProviderGuidance(cwd, provider, planned) {
9237
- if (provider === "claude" || provider === "both") {
9238
- if (!existsSync17(join20(cwd, "CLAUDE.md"))) {
9239
- planned.push({ src: ASSET.claudeTemplate, dest: join20(cwd, "CLAUDE.md"), rel: "CLAUDE.md" });
9240
- }
9241
- if (!existsSync17(join20(cwd, "AGENTS.md"))) {
9242
- planned.push({ src: ASSET.agentsTemplate, dest: join20(cwd, "AGENTS.md"), rel: "AGENTS.md" });
9719
+ function checkCodeMap(cwd) {
9720
+ const findings = [];
9721
+ const mapPath = join21(cwd, ".cdd", "code-map.yml");
9722
+ if (!existsSync18(mapPath)) {
9723
+ const probe2 = checkCodeMapFreshness(cwd);
9724
+ if (probe2.status === "config-error") {
9725
+ findings.push({ level: "warning", message: `.cdd/code-map-config.yml is invalid: ${probe2.configError}` });
9726
+ } else if (probe2.status === "missing-with-sources") {
9727
+ findings.push({ level: "warning", message: ".cdd/code-map.yml is missing; run `cdd-kit code-map`" });
9243
9728
  }
9729
+ return findings;
9730
+ }
9731
+ const probe = checkCodeMapFreshness(cwd);
9732
+ if (probe.status === "config-error") {
9733
+ findings.push({ level: "warning", message: `.cdd/code-map-config.yml is invalid: ${probe.configError}` });
9734
+ return findings;
9244
9735
  }
9245
- if ((provider === "codex" || provider === "both") && !existsSync17(join20(cwd, "CODEX.md"))) {
9246
- planned.push({ src: ASSET.codexTemplate, dest: join20(cwd, "CODEX.md"), rel: "CODEX.md" });
9736
+ if (probe.status === "stale") {
9737
+ const top = probe.staleFiles.slice(0, 3).join(", ");
9738
+ const more = probe.staleCount > 3 ? ` (+${probe.staleCount - 3} more)` : "";
9739
+ findings.push({ level: "warning", message: `code-map stale: ${top}${more}; run \`cdd-kit code-map\`` });
9247
9740
  }
9248
- }
9249
- function applyCopy(plan) {
9250
- for (const item of plan) {
9251
- mkdirSync8(dirname5(item.dest), { recursive: true });
9252
- copyFileSync3(item.src, item.dest);
9741
+ const text = readFileSync19(mapPath, "utf8");
9742
+ const m = text.match(/^# files: (\d+), src-lines: (\d+), map-lines: (\d+), compression: ([\d.]+)x/m);
9743
+ if (m) {
9744
+ findings.push({ level: "ok", message: `code-map: ${m[1]} files, ${m[4]}x compression` });
9745
+ } else {
9746
+ findings.push({ level: "warning", message: ".cdd/code-map.yml has no metadata header (regenerate with cdd-kit 2.0.5+)" });
9253
9747
  }
9748
+ return findings;
9254
9749
  }
9255
- async function upgrade(opts = {}) {
9256
- const cwd = process.cwd();
9750
+ async function buildDoctorReport(cwd, opts) {
9257
9751
  const requestedProvider = opts.provider ?? "auto";
9258
9752
  if (!validateProviderOption(requestedProvider)) {
9259
9753
  log.error(`Invalid provider: ${requestedProvider}. Use auto, claude, codex, or both.`);
9260
9754
  process.exit(1);
9261
9755
  }
9756
+ const strict = opts.strict ?? false;
9262
9757
  const provider = inferProvider(cwd, requestedProvider);
9263
- const plan = [];
9264
- planMissingFiles(ASSET.contracts, join20(cwd, "contracts"), "contracts", plan);
9265
- planMissingFiles(ASSET.specsTemplates, join20(cwd, "specs", "templates"), "specs/templates", plan);
9266
- planMissingFiles(ASSET.testsTemplates, join20(cwd, "tests", "templates"), "tests/templates", plan);
9267
- planMissingFiles(ASSET.ci, join20(cwd, "ci"), "ci", plan);
9268
- planMissingFiles(ASSET.githubWorkflows, join20(cwd, ".github", "workflows"), ".github/workflows", plan);
9269
- planMissingFiles(ASSET.cddConfig, join20(cwd, ".cdd"), ".cdd", plan);
9270
- planProviderGuidance(cwd, provider, plan);
9271
- log.blank();
9272
- log.info(`Upgrade provider: ${provider}`);
9273
- if (plan.length === 0) {
9274
- log.ok("No missing cdd-kit project files found.");
9275
- if (opts.migrateChanges) {
9276
- log.blank();
9277
- log.info("Running change migration flow...");
9278
- await migrate(void 0, {
9279
- all: true,
9280
- dryRun: !opts.yes,
9281
- enableContextGovernance: opts.enableContextGovernance
9282
- });
9283
- }
9284
- log.blank();
9285
- return;
9758
+ const findings = [];
9759
+ for (const relPath of ["contracts", "specs/templates", ".cdd/context-policy.json", ".cdd/model-policy.json"]) {
9760
+ findings.push(fileExists(cwd, relPath) ? { level: "ok", message: `${relPath} exists` } : { level: "warning", message: `${relPath} is missing; run cdd-kit upgrade --yes` });
9286
9761
  }
9287
- log.info(`${plan.length} missing file(s) detected:`);
9288
- for (const item of plan)
9289
- log.dim(` + ${item.rel.replace(/\\/g, "/")}`);
9290
- if (!opts.yes) {
9291
- log.blank();
9292
- log.info("Dry run only. Re-run with --yes to write missing files.");
9293
- if (opts.migrateChanges) {
9294
- log.blank();
9295
- log.info("Previewing existing change migration because --migrate-changes was requested.");
9296
- await migrate(void 0, {
9297
- all: true,
9298
- dryRun: true,
9299
- enableContextGovernance: opts.enableContextGovernance
9762
+ if ((provider === "claude" || provider === "both") && !fileExists(cwd, "CLAUDE.md")) {
9763
+ findings.push({ level: "warning", message: "CLAUDE.md is missing for Claude provider; run cdd-kit upgrade --provider claude --yes" });
9764
+ }
9765
+ if ((provider === "claude" || provider === "both") && !fileExists(cwd, "AGENTS.md")) {
9766
+ findings.push({ level: "warning", message: "AGENTS.md is missing for Claude provider; run cdd-kit upgrade --provider claude --yes" });
9767
+ }
9768
+ if ((provider === "codex" || provider === "both") && !fileExists(cwd, "CODEX.md")) {
9769
+ findings.push({ level: "warning", message: "CODEX.md is missing for Codex provider; run cdd-kit upgrade --provider codex --yes" });
9770
+ }
9771
+ findings.push(...checkContextFreshness(cwd));
9772
+ findings.push(...checkModelPolicyDrift(cwd));
9773
+ findings.push(...await checkAgentLint(cwd));
9774
+ findings.push(...checkCodeMap(cwd));
9775
+ const errors = findings.filter((finding) => finding.level === "error").length;
9776
+ const warnings = findings.filter((finding) => finding.level === "warning").length;
9777
+ return {
9778
+ provider,
9779
+ strict,
9780
+ findings,
9781
+ errors,
9782
+ warnings,
9783
+ ok: errors === 0 && (!strict || warnings === 0)
9784
+ };
9785
+ }
9786
+ async function attemptAutoFixes(cwd, report) {
9787
+ const fixed = [];
9788
+ const remaining = [];
9789
+ for (const finding of report.findings) {
9790
+ if (finding.level !== "warning") {
9791
+ remaining.push(finding);
9792
+ continue;
9793
+ }
9794
+ if (/specs\/context indexes are missing|inputs changed|older cdd-kit|older than/i.test(finding.message)) {
9795
+ try {
9796
+ const { contextScan: contextScan2 } = await Promise.resolve().then(() => (init_context_scan(), context_scan_exports));
9797
+ await contextScan2();
9798
+ fixed.push(`ran context-scan to refresh specs/context/`);
9799
+ continue;
9800
+ } catch (err) {
9801
+ remaining.push({ level: "warning", message: `${finding.message} (auto-fix failed: ${err.message})` });
9802
+ continue;
9803
+ }
9804
+ }
9805
+ if (/code-map stale|code-map\.yml is missing/i.test(finding.message)) {
9806
+ try {
9807
+ const { codeMap: codeMap2 } = await Promise.resolve().then(() => (init_code_map(), code_map_exports));
9808
+ await codeMap2({ path: ".", out: ".cdd/code-map.yml", include: [], exclude: [], check: false, maxLines: 1e5 });
9809
+ fixed.push("regenerated .cdd/code-map.yml");
9810
+ continue;
9811
+ } catch (err) {
9812
+ remaining.push({ level: "warning", message: `${finding.message} (auto-fix failed: ${err.message})` });
9813
+ continue;
9814
+ }
9815
+ }
9816
+ if (/model-policy\.json has no role bindings/i.test(finding.message)) {
9817
+ const policyPath = join21(cwd, ".cdd", "model-policy.json");
9818
+ try {
9819
+ let existing = {};
9820
+ try {
9821
+ existing = JSON.parse(readFileSync19(policyPath, "utf8"));
9822
+ } catch {
9823
+ }
9824
+ const merged = {
9825
+ ...existing,
9826
+ roles: {
9827
+ "change-classifier": "claude-opus-4-7",
9828
+ "spec-architect": "claude-opus-4-7",
9829
+ "qa-reviewer": "claude-opus-4-7",
9830
+ "contract-reviewer": "claude-sonnet-4-6",
9831
+ "test-strategist": "claude-sonnet-4-6",
9832
+ "backend-engineer": "claude-sonnet-4-6",
9833
+ "frontend-engineer": "claude-sonnet-4-6",
9834
+ "ci-cd-gatekeeper": "claude-sonnet-4-6",
9835
+ "e2e-resilience-engineer": "claude-sonnet-4-6",
9836
+ "monkey-test-engineer": "claude-sonnet-4-6",
9837
+ "stress-soak-engineer": "claude-sonnet-4-6",
9838
+ "ui-ux-reviewer": "claude-sonnet-4-6",
9839
+ "visual-reviewer": "claude-haiku-4-5-20251001",
9840
+ "dependency-security-reviewer": "claude-sonnet-4-6",
9841
+ "spec-drift-auditor": "claude-opus-4-7",
9842
+ "repo-context-scanner": "claude-haiku-4-5-20251001"
9843
+ }
9844
+ };
9845
+ const { writeFileSync: writeFileSync14 } = await import("fs");
9846
+ writeFileSync14(policyPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
9847
+ fixed.push(`populated .cdd/model-policy.json with default role bindings`);
9848
+ continue;
9849
+ } catch (err) {
9850
+ remaining.push({ level: "warning", message: `${finding.message} (auto-fix failed: ${err.message})` });
9851
+ continue;
9852
+ }
9853
+ }
9854
+ if (/\.cdd\/.*is missing|run cdd-kit upgrade/i.test(finding.message)) {
9855
+ remaining.push({
9856
+ level: "warning",
9857
+ message: `${finding.message} (run \`cdd-kit upgrade --yes\` manually \u2014 too invasive for --fix)`
9300
9858
  });
9859
+ continue;
9301
9860
  }
9861
+ remaining.push(finding);
9862
+ }
9863
+ return { fixed, remaining };
9864
+ }
9865
+ async function doctor(opts = {}) {
9866
+ const cwd = process.cwd();
9867
+ let report = await buildDoctorReport(cwd, opts);
9868
+ if (opts.fix && !opts.json) {
9302
9869
  log.blank();
9870
+ log.info("Doctor --fix: attempting safe auto-resolutions\u2026");
9871
+ const { fixed, remaining } = await attemptAutoFixes(cwd, report);
9872
+ for (const f of fixed)
9873
+ log.ok(`fixed: ${f}`);
9874
+ if (fixed.length > 0) {
9875
+ report = await buildDoctorReport(cwd, opts);
9876
+ } else {
9877
+ log.info("no auto-fixable findings");
9878
+ }
9879
+ }
9880
+ if (opts.json) {
9881
+ console.log(JSON.stringify(report, null, 2));
9882
+ if (!report.ok)
9883
+ process.exit(1);
9303
9884
  return;
9304
9885
  }
9305
- applyCopy(plan);
9306
- const modelPolicyPath = join20(cwd, ".cdd", "model-policy.json");
9307
- if (existsSync17(modelPolicyPath)) {
9308
- let existing = {};
9309
- try {
9310
- existing = JSON.parse(readFileSync18(modelPolicyPath, "utf8"));
9311
- } catch {
9312
- }
9313
- const merged = {
9314
- ...existing,
9315
- provider,
9316
- generated_at: (/* @__PURE__ */ new Date()).toISOString(),
9317
- roles: existing.roles && typeof existing.roles === "object" ? existing.roles : {}
9318
- };
9319
- writeFileSync9(modelPolicyPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
9886
+ log.blank();
9887
+ log.info(`Doctor provider: ${report.provider}`);
9888
+ for (const finding of report.findings) {
9889
+ if (finding.level === "ok")
9890
+ log.ok(finding.message);
9891
+ else if (finding.level === "warning")
9892
+ log.warn(finding.message);
9893
+ else
9894
+ log.error(finding.message);
9320
9895
  }
9321
9896
  log.blank();
9322
- log.ok(`Upgrade complete: ${plan.length} missing file(s) added.`);
9323
- log.info("Existing project guidance and contracts were preserved.");
9324
- if (opts.migrateChanges) {
9325
- log.blank();
9326
- log.info("Running change migration flow...");
9327
- await migrate(void 0, {
9328
- all: true,
9329
- dryRun: false,
9330
- enableContextGovernance: opts.enableContextGovernance
9331
- });
9897
+ if (!report.ok) {
9898
+ log.error(report.strict && report.errors === 0 ? `doctor failed in strict mode with ${report.warnings} warning(s)` : `doctor failed with ${report.errors} error(s)`);
9899
+ process.exit(1);
9900
+ }
9901
+ if (report.warnings > 0) {
9902
+ log.warn(`doctor completed with ${report.warnings} warning(s)`);
9903
+ } else {
9904
+ log.ok("doctor passed");
9332
9905
  }
9333
9906
  log.blank();
9334
9907
  }
9335
- var init_upgrade = __esm({
9336
- "src/commands/upgrade.ts"() {
9908
+ var init_doctor = __esm({
9909
+ "src/commands/doctor.ts"() {
9337
9910
  "use strict";
9338
- init_paths();
9339
9911
  init_logger();
9340
9912
  init_provider();
9341
- init_migrate();
9913
+ init_freshness();
9342
9914
  }
9343
9915
  });
9344
9916
 
@@ -9347,8 +9919,8 @@ var lint_agents_exports = {};
9347
9919
  __export(lint_agents_exports, {
9348
9920
  lintAgents: () => lintAgents
9349
9921
  });
9350
- import { readdirSync as readdirSync11, readFileSync as readFileSync19 } from "fs";
9351
- import { join as join21 } from "path";
9922
+ import { readdirSync as readdirSync12, readFileSync as readFileSync20 } from "fs";
9923
+ import { join as join22 } from "path";
9352
9924
  import { load as yamlLoad2 } from "js-yaml";
9353
9925
  function extractRequiredArtifactsSection(content) {
9354
9926
  const match = content.match(
@@ -9389,20 +9961,20 @@ function hasFlatBacktickKeysWithoutFence(section) {
9389
9961
  }
9390
9962
  async function lintAgents(opts) {
9391
9963
  const cwd = process.cwd();
9392
- const agentsDir = join21(cwd, ".claude", "agents");
9964
+ const agentsDir = join22(cwd, ".claude", "agents");
9393
9965
  let files;
9394
9966
  try {
9395
- files = readdirSync11(agentsDir).filter((f) => f.endsWith(".md")).sort();
9967
+ files = readdirSync12(agentsDir).filter((f) => f.endsWith(".md")).sort();
9396
9968
  } catch {
9397
9969
  log.error(`lint-agents: cannot read ${agentsDir} \u2014 is this a cdd-kit project?`);
9398
9970
  return 1;
9399
9971
  }
9400
9972
  const violations = [];
9401
9973
  for (const filename of files) {
9402
- const filePath = join21(agentsDir, filename);
9974
+ const filePath = join22(agentsDir, filename);
9403
9975
  let content;
9404
9976
  try {
9405
- content = readFileSync19(filePath, "utf8");
9977
+ content = readFileSync20(filePath, "utf8");
9406
9978
  } catch {
9407
9979
  violations.push({
9408
9980
  file: filename,
@@ -9514,28 +10086,28 @@ var archive_exports = {};
9514
10086
  __export(archive_exports, {
9515
10087
  archive: () => archive
9516
10088
  });
9517
- import { join as join22 } from "path";
9518
- import { existsSync as existsSync18, mkdirSync as mkdirSync9, renameSync as renameSync2, readFileSync as readFileSync20, writeFileSync as writeFileSync10, appendFileSync, cpSync as cpSync3, rmSync as rmSync3 } from "fs";
10089
+ import { join as join23 } from "path";
10090
+ import { existsSync as existsSync19, mkdirSync as mkdirSync10, renameSync as renameSync2, readFileSync as readFileSync21, writeFileSync as writeFileSync11, appendFileSync, cpSync as cpSync3, rmSync as rmSync3 } from "fs";
9519
10091
  import yaml4 from "js-yaml";
9520
10092
  async function archive(changeId) {
9521
10093
  const cwd = process.cwd();
9522
- const changeDir = join22(cwd, "specs", "changes", changeId);
10094
+ const changeDir = join23(cwd, "specs", "changes", changeId);
9523
10095
  const archiveYear = (/* @__PURE__ */ new Date()).getFullYear().toString();
9524
- const archiveBase = join22(cwd, "specs", "archive", archiveYear);
9525
- const archiveDir = join22(archiveBase, changeId);
9526
- const indexPath = join22(cwd, "specs", "archive", "INDEX.md");
9527
- if (!existsSync18(changeDir)) {
10096
+ const archiveBase = join23(cwd, "specs", "archive", archiveYear);
10097
+ const archiveDir = join23(archiveBase, changeId);
10098
+ const indexPath = join23(cwd, "specs", "archive", "INDEX.md");
10099
+ if (!existsSync19(changeDir)) {
9528
10100
  log.error(`Change not found: specs/changes/${changeId}`);
9529
10101
  process.exit(1);
9530
10102
  }
9531
- if (existsSync18(archiveDir)) {
10103
+ if (existsSync19(archiveDir)) {
9532
10104
  log.error(`Already archived: specs/archive/${archiveYear}/${changeId}`);
9533
10105
  process.exit(1);
9534
10106
  }
9535
- const tasksPath = join22(changeDir, "tasks.yml");
9536
- if (existsSync18(tasksPath)) {
10107
+ const tasksPath = join23(changeDir, "tasks.yml");
10108
+ if (existsSync19(tasksPath)) {
9537
10109
  try {
9538
- const raw = readFileSync20(tasksPath, "utf8");
10110
+ const raw = readFileSync21(tasksPath, "utf8");
9539
10111
  const data = yaml4.load(raw);
9540
10112
  if (data?.status === "gate-blocked") {
9541
10113
  log.warn("tasks.yml has status: gate-blocked \u2014 archiving anyway (change was paused).");
@@ -9548,8 +10120,8 @@ async function archive(changeId) {
9548
10120
  log.warn("tasks.yml could not be parsed \u2014 archiving anyway.");
9549
10121
  }
9550
10122
  }
9551
- if (!existsSync18(archiveBase)) {
9552
- mkdirSync9(archiveBase, { recursive: true });
10123
+ if (!existsSync19(archiveBase)) {
10124
+ mkdirSync10(archiveBase, { recursive: true });
9553
10125
  }
9554
10126
  try {
9555
10127
  renameSync2(changeDir, archiveDir);
@@ -9565,8 +10137,8 @@ async function archive(changeId) {
9565
10137
  const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
9566
10138
  const indexLine = `| ${changeId} | ${archiveYear} | ${today} | specs/archive/${archiveYear}/${changeId}/ |
9567
10139
  `;
9568
- if (!existsSync18(indexPath)) {
9569
- writeFileSync10(indexPath, `# Archive Index
10140
+ if (!existsSync19(indexPath)) {
10141
+ writeFileSync11(indexPath, `# Archive Index
9570
10142
 
9571
10143
  | change-id | year | archived-date | path |
9572
10144
  |---|---|---|---|
@@ -9590,37 +10162,37 @@ var abandon_exports = {};
9590
10162
  __export(abandon_exports, {
9591
10163
  abandon: () => abandon
9592
10164
  });
9593
- import { join as join23 } from "path";
9594
- import { existsSync as existsSync19, readFileSync as readFileSync21, writeFileSync as writeFileSync11, appendFileSync as appendFileSync2, mkdirSync as mkdirSync10 } from "fs";
10165
+ import { join as join24 } from "path";
10166
+ import { existsSync as existsSync20, readFileSync as readFileSync22, writeFileSync as writeFileSync12, appendFileSync as appendFileSync2, mkdirSync as mkdirSync11 } from "fs";
9595
10167
  import yaml5 from "js-yaml";
9596
10168
  async function abandon(changeId, opts) {
9597
10169
  const cwd = process.cwd();
9598
- const changeDir = join23(cwd, "specs", "changes", changeId);
9599
- const tasksPath = join23(changeDir, "tasks.yml");
9600
- if (!existsSync19(changeDir)) {
10170
+ const changeDir = join24(cwd, "specs", "changes", changeId);
10171
+ const tasksPath = join24(changeDir, "tasks.yml");
10172
+ if (!existsSync20(changeDir)) {
9601
10173
  log.error(`Change not found: specs/changes/${changeId}`);
9602
10174
  process.exit(1);
9603
10175
  }
9604
- if (existsSync19(tasksPath)) {
9605
- const raw = readFileSync21(tasksPath, "utf8");
10176
+ if (existsSync20(tasksPath)) {
10177
+ const raw = readFileSync22(tasksPath, "utf8");
9606
10178
  const data = yaml5.load(raw) ?? {};
9607
10179
  data["status"] = "abandoned";
9608
10180
  if (!data["change-id"]) {
9609
10181
  data["change-id"] = changeId;
9610
10182
  }
9611
- writeFileSync11(tasksPath, yaml5.dump(data, { lineWidth: -1, noRefs: true }), "utf8");
10183
+ writeFileSync12(tasksPath, yaml5.dump(data, { lineWidth: -1, noRefs: true }), "utf8");
9612
10184
  }
9613
10185
  const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
9614
- const archiveDir = join23(cwd, "specs", "archive");
9615
- const indexPath = join23(archiveDir, "INDEX.md");
10186
+ const archiveDir = join24(cwd, "specs", "archive");
10187
+ const indexPath = join24(archiveDir, "INDEX.md");
9616
10188
  const reason = opts.reason ?? "no reason given";
9617
10189
  const indexLine = `| ${changeId} | abandoned | ${today} | ${reason} |
9618
10190
  `;
9619
- if (!existsSync19(archiveDir)) {
9620
- mkdirSync10(archiveDir, { recursive: true });
10191
+ if (!existsSync20(archiveDir)) {
10192
+ mkdirSync11(archiveDir, { recursive: true });
9621
10193
  }
9622
- if (!existsSync19(indexPath)) {
9623
- writeFileSync11(indexPath, `# Archive Index
10194
+ if (!existsSync20(indexPath)) {
10195
+ writeFileSync12(indexPath, `# Archive Index
9624
10196
 
9625
10197
  | change-id | status | date | notes |
9626
10198
  |---|---|---|---|
@@ -9644,28 +10216,28 @@ var list_changes_exports = {};
9644
10216
  __export(list_changes_exports, {
9645
10217
  listChanges: () => listChanges
9646
10218
  });
9647
- import { join as join24 } from "path";
9648
- import { existsSync as existsSync20, readdirSync as readdirSync12, readFileSync as readFileSync22 } from "fs";
10219
+ import { join as join25 } from "path";
10220
+ import { existsSync as existsSync21, readdirSync as readdirSync13, readFileSync as readFileSync23 } from "fs";
9649
10221
  import yaml6 from "js-yaml";
9650
10222
  async function listChanges() {
9651
10223
  const cwd = process.cwd();
9652
- const changesDir = join24(cwd, "specs", "changes");
10224
+ const changesDir = join25(cwd, "specs", "changes");
9653
10225
  log.blank();
9654
10226
  const active = [];
9655
- if (existsSync20(changesDir)) {
9656
- active.push(...readdirSync12(changesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name));
10227
+ if (existsSync21(changesDir)) {
10228
+ active.push(...readdirSync13(changesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name));
9657
10229
  }
9658
10230
  if (active.length === 0) {
9659
10231
  log.info("No active changes in specs/changes/");
9660
10232
  } else {
9661
10233
  log.info("Active changes:");
9662
10234
  for (const id of active) {
9663
- const tasksPath = join24(changesDir, id, "tasks.yml");
10235
+ const tasksPath = join25(changesDir, id, "tasks.yml");
9664
10236
  let status = "in-progress";
9665
10237
  let pending = 0;
9666
- if (existsSync20(tasksPath)) {
10238
+ if (existsSync21(tasksPath)) {
9667
10239
  try {
9668
- const raw = readFileSync22(tasksPath, "utf8");
10240
+ const raw = readFileSync23(tasksPath, "utf8");
9669
10241
  const data = yaml6.load(raw);
9670
10242
  if (data?.status)
9671
10243
  status = data.status;
@@ -9696,8 +10268,8 @@ __export(context_exports, {
9696
10268
  rejectContextExpansion: () => rejectContextExpansion,
9697
10269
  requestContextExpansion: () => requestContextExpansion
9698
10270
  });
9699
- import { existsSync as existsSync21, readFileSync as readFileSync23, writeFileSync as writeFileSync12 } from "fs";
9700
- import { join as join25 } from "path";
10271
+ import { existsSync as existsSync22, readFileSync as readFileSync24, writeFileSync as writeFileSync13 } from "fs";
10272
+ import { join as join26 } from "path";
9701
10273
  function normalizePath(path) {
9702
10274
  return path.replace(/\\/g, "/").replace(/^\.\//, "").trim();
9703
10275
  }
@@ -9711,18 +10283,18 @@ function validateRepoRelativePath(path) {
9711
10283
  return null;
9712
10284
  }
9713
10285
  function manifestPathFor(changeId) {
9714
- return join25(process.cwd(), "specs", "changes", changeId, "context-manifest.md");
10286
+ return join26(process.cwd(), "specs", "changes", changeId, "context-manifest.md");
9715
10287
  }
9716
10288
  function readManifest(changeId) {
9717
10289
  const manifestPath = manifestPathFor(changeId);
9718
- if (!existsSync21(manifestPath)) {
10290
+ if (!existsSync22(manifestPath)) {
9719
10291
  log.error(`context manifest not found: specs/changes/${changeId}/context-manifest.md`);
9720
10292
  process.exit(1);
9721
10293
  }
9722
- return readFileSync23(manifestPath, "utf8");
10294
+ return readFileSync24(manifestPath, "utf8");
9723
10295
  }
9724
10296
  function writeManifest(changeId, content) {
9725
- writeFileSync12(manifestPathFor(changeId), content.endsWith("\n") ? content : `${content}
10297
+ writeFileSync13(manifestPathFor(changeId), content.endsWith("\n") ? content : `${content}
9726
10298
  `, "utf8");
9727
10299
  }
9728
10300
  function sectionBody(content, heading) {
@@ -9947,9 +10519,9 @@ var init_context = __esm({
9947
10519
  });
9948
10520
 
9949
10521
  // src/cli/index.ts
9950
- import { readFileSync as readFileSync24 } from "fs";
10522
+ import { readFileSync as readFileSync25 } from "fs";
9951
10523
  import { fileURLToPath as fileURLToPath3 } from "url";
9952
- import { dirname as dirname6, join as join26 } from "path";
10524
+ import { dirname as dirname7, join as join27 } from "path";
9953
10525
  import { Command } from "commander";
9954
10526
 
9955
10527
  // src/commands/init.ts
@@ -10364,167 +10936,8 @@ async function init(opts) {
10364
10936
  log.blank();
10365
10937
  }
10366
10938
 
10367
- // src/commands/update.ts
10368
- init_paths();
10369
- init_logger();
10370
- init_provider();
10371
- import { join as join7 } from "path";
10372
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, readdirSync as readdirSync3, copyFileSync as copyFileSync2, readFileSync as readFileSync5 } from "fs";
10373
- import { createHash } from "crypto";
10374
- import { homedir as homedir2 } from "os";
10375
- function fileHash(filePath) {
10376
- const buf = readFileSync5(filePath);
10377
- return createHash("sha256").update(buf).digest("hex");
10378
- }
10379
- function diffDir(src, dest) {
10380
- const entries = [];
10381
- if (!existsSync6(src))
10382
- return entries;
10383
- function walk(currentSrc, currentDest) {
10384
- const items = readdirSync3(currentSrc, { withFileTypes: true });
10385
- for (const item of items) {
10386
- const srcPath = join7(currentSrc, item.name);
10387
- const destPath = join7(currentDest, item.name);
10388
- if (item.isDirectory()) {
10389
- walk(srcPath, destPath);
10390
- } else {
10391
- if (!existsSync6(destPath)) {
10392
- entries.push({ src: srcPath, dest: destPath, action: "add" });
10393
- } else if (fileHash(srcPath) !== fileHash(destPath)) {
10394
- entries.push({ src: srcPath, dest: destPath, action: "overwrite" });
10395
- } else {
10396
- entries.push({ src: srcPath, dest: destPath, action: "skip" });
10397
- }
10398
- }
10399
- }
10400
- }
10401
- walk(src, dest);
10402
- return entries;
10403
- }
10404
- function applyDir(entries) {
10405
- let count = 0;
10406
- for (const e of entries) {
10407
- if (e.action === "skip")
10408
- continue;
10409
- mkdirSync3(join7(e.dest, ".."), { recursive: true });
10410
- copyFileSync2(e.src, e.dest);
10411
- count += 1;
10412
- }
10413
- return count;
10414
- }
10415
- function backupDir(dir, backupDest) {
10416
- if (!existsSync6(dir))
10417
- return;
10418
- mkdirSync3(backupDest, { recursive: true });
10419
- function walk(src, dst) {
10420
- const items = readdirSync3(src, { withFileTypes: true });
10421
- for (const item of items) {
10422
- const s = join7(src, item.name);
10423
- const d = join7(dst, item.name);
10424
- if (item.isDirectory()) {
10425
- mkdirSync3(d, { recursive: true });
10426
- walk(s, d);
10427
- } else
10428
- copyFileSync2(s, d);
10429
- }
10430
- }
10431
- walk(dir, backupDest);
10432
- }
10433
- async function update(opts) {
10434
- if (opts.postinstall) {
10435
- if (!existsSync6(join7(SKILLS_HOME, "contract-driven-delivery"))) {
10436
- return;
10437
- }
10438
- opts.yes = true;
10439
- opts.provider = "claude";
10440
- }
10441
- const quiet = !!opts.postinstall;
10442
- if (!quiet)
10443
- log.blank();
10444
- const cwd = process.cwd();
10445
- const requestedProvider = opts.provider ?? "auto";
10446
- if (!validateProviderOption(requestedProvider)) {
10447
- log.error(`Invalid provider: ${requestedProvider}. Use auto, claude, codex, or both.`);
10448
- process.exit(1);
10449
- }
10450
- const provider = inferProvider(cwd, requestedProvider);
10451
- const updateClaudeAssets = provider === "claude" || provider === "both";
10452
- const agentDiff = updateClaudeAssets ? diffDir(ASSET.agents, AGENTS_HOME) : [];
10453
- const skillDiff = updateClaudeAssets ? readdirSync3(ASSET.skills, { withFileTypes: true }).filter((d) => d.isDirectory()).flatMap((d) => diffDir(join7(ASSET.skills, d.name), join7(SKILLS_HOME, d.name))) : [];
10454
- const toWrite = [...agentDiff, ...skillDiff].filter((e) => e.action !== "skip");
10455
- const toAdd = toWrite.filter((e) => e.action === "add");
10456
- const toOver = toWrite.filter((e) => e.action === "overwrite");
10457
- const toSkip = [...agentDiff, ...skillDiff].filter((e) => e.action === "skip");
10458
- if (!quiet) {
10459
- log.info(`Provider: ${provider}`);
10460
- if (updateClaudeAssets) {
10461
- log.info(`Dry-run diff \u2014 agents: ${AGENTS_HOME}`);
10462
- log.info(`Dry-run diff \u2014 skills: ${SKILLS_HOME}`);
10463
- } else {
10464
- log.info("Codex provider has no global cdd-kit assets to update.");
10465
- log.info("Project files are preserved; run cdd-kit init --local-only --provider codex to add missing local guidance.");
10466
- }
10467
- log.blank();
10468
- if (toAdd.length)
10469
- log.info(` + ${toAdd.length} file(s) would be added`);
10470
- if (toOver.length)
10471
- log.warn(` ~ ${toOver.length} file(s) would be overwritten (user edits lost without backup)`);
10472
- if (toSkip.length)
10473
- log.dim(` ${toSkip.length} file(s) unchanged (skipped)`);
10474
- }
10475
- if (toWrite.length === 0) {
10476
- if (!quiet) {
10477
- log.blank();
10478
- log.ok("Already up to date \u2014 nothing to write.");
10479
- log.blank();
10480
- }
10481
- return;
10482
- }
10483
- if (!quiet && !opts.yes) {
10484
- log.blank();
10485
- log.info("Run with --yes to apply changes. Example:");
10486
- log.dim(" cdd-kit update --yes");
10487
- log.blank();
10488
- return;
10489
- }
10490
- const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
10491
- const backupRoot = join7(homedir2(), ".claude", ".cdd-kit-backup", timestamp);
10492
- if (!quiet) {
10493
- log.blank();
10494
- log.info(`Backing up to ${backupRoot} \u2026`);
10495
- }
10496
- backupDir(AGENTS_HOME, join7(backupRoot, "agents"));
10497
- backupDir(SKILLS_HOME, join7(backupRoot, "skills"));
10498
- if (!quiet)
10499
- log.ok(`Backup complete: ${backupRoot}`);
10500
- if (!quiet)
10501
- log.blank();
10502
- let totalSynced = 0;
10503
- if (updateClaudeAssets) {
10504
- if (!quiet)
10505
- log.info(`Updating agents \u2192 ${AGENTS_HOME}`);
10506
- const agentCount = applyDir(agentDiff);
10507
- if (!quiet)
10508
- log.ok(`${agentCount} agent file(s) updated.`);
10509
- totalSynced += agentCount;
10510
- if (!quiet)
10511
- log.info(`Updating skills \u2192 ${SKILLS_HOME}`);
10512
- const skillCount = applyDir(skillDiff);
10513
- if (!quiet)
10514
- log.ok(`${skillCount} skill file(s) updated.`);
10515
- totalSynced += skillCount;
10516
- }
10517
- if (quiet) {
10518
- if (totalSynced > 0)
10519
- log.ok(`cdd-kit: synced ${totalSynced} file(s) to ~/.claude/`);
10520
- } else {
10521
- log.blank();
10522
- log.info("Project files (contracts/, specs/, tests/, ci/) were not changed.");
10523
- log.ok("Update complete.");
10524
- log.info(`Backup saved to: ${backupRoot}`);
10525
- log.blank();
10526
- }
10527
- }
10939
+ // src/cli/index.ts
10940
+ init_update();
10528
10941
 
10529
10942
  // src/commands/new-change.ts
10530
10943
  init_paths();
@@ -11513,8 +11926,8 @@ async function installHooks() {
11513
11926
  }
11514
11927
 
11515
11928
  // src/cli/index.ts
11516
- var __dirname2 = dirname6(fileURLToPath3(import.meta.url));
11517
- var pkg = JSON.parse(readFileSync24(join26(__dirname2, "..", "..", "package.json"), "utf8"));
11929
+ var __dirname2 = dirname7(fileURLToPath3(import.meta.url));
11930
+ var pkg = JSON.parse(readFileSync25(join27(__dirname2, "..", "..", "package.json"), "utf8"));
11518
11931
  var program = new Command();
11519
11932
  program.name("cdd-kit").description("Contract-Driven Delivery Kit CLI").version(pkg.version);
11520
11933
  program.command("init").description(
@@ -11529,6 +11942,18 @@ program.command("init").description(
11529
11942
  })
11530
11943
  );
11531
11944
  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 }));
11945
+ program.command("refresh").description("Complete upgrade: refresh agents, skills, templates, hooks, model-policy, and code-map in one command").option("--yes", "Apply changes (default is dry-run)", false).option("--no-templates", "Skip force-refresh of specs/templates, tests/templates, ci-templates, .github/workflows").option("--no-hooks", "Skip pre-commit hook re-installation").option("--no-code-map", "Skip code-map regeneration").option("--no-update", "Skip ~/.claude update step").option("--no-upgrade", "Skip project add-missing step").option("--provider <provider>", "Provider adapter: auto, claude, codex, or both", "auto").action(async (opts) => {
11946
+ const { refresh: refresh2 } = await Promise.resolve().then(() => (init_refresh(), refresh_exports));
11947
+ await refresh2({
11948
+ yes: opts.yes,
11949
+ noTemplates: opts.templates === false,
11950
+ noHooks: opts.hooks === false,
11951
+ noCodeMap: opts.codeMap === false,
11952
+ noUpdate: opts.update === false,
11953
+ noUpgrade: opts.upgrade === false,
11954
+ provider: opts.provider
11955
+ });
11956
+ });
11532
11957
  program.command("doctor").description("Inspect cdd-kit repo health, provider guidance, and context index freshness").option("--strict", "Treat warnings as errors", false).option("--json", "Print a machine-readable health report", false).option("--provider <provider>", "Provider adapter to inspect: auto, claude, codex, or both", "auto").option("--fix", "Auto-resolve safe warnings (stale context indexes, missing role bindings)", false).action(async (opts) => {
11533
11958
  const { doctor: doctor2 } = await Promise.resolve().then(() => (init_doctor(), doctor_exports));
11534
11959
  await doctor2({ strict: opts.strict, json: opts.json, provider: opts.provider, fix: opts.fix });