contract-driven-delivery 2.0.9 → 2.0.11

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 +88 -0
  2. package/dist/cli/index.js +1639 -1584
  3. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -376,24 +376,44 @@ var init_update = __esm({
376
376
  }
377
377
  });
378
378
 
379
- // src/commands/context-scan.ts
380
- var context_scan_exports = {};
381
- __export(context_scan_exports, {
382
- contextScan: () => contextScan
383
- });
384
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync6, readdirSync as readdirSync4, writeFileSync as writeFileSync3 } from "fs";
379
+ // src/utils/digest.ts
380
+ import { readFileSync as readFileSync6 } from "fs";
385
381
  import { createHash as createHash2 } from "crypto";
386
- import { basename, dirname as dirname3, join as join8, relative as relative2 } from "path";
387
- function sha256OfFile(path) {
382
+ function normalizeContentForHash(buf) {
383
+ if (!buf.includes(13))
384
+ return buf;
385
+ const text = buf.toString("utf8").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
386
+ return Buffer.from(text, "utf8");
387
+ }
388
+ function sha256OfFileNormalized(path) {
389
+ let buf;
388
390
  try {
389
- return createHash2("sha256").update(readFileSync6(path)).digest("hex");
391
+ buf = readFileSync6(path);
390
392
  } catch {
391
393
  return "";
392
394
  }
395
+ return createHash2("sha256").update(normalizeContentForHash(buf)).digest("hex");
393
396
  }
394
- function inputsDigest(paths) {
395
- const combined = paths.slice().sort().map((p) => `${p}:${sha256OfFile(p)}`).join("\n");
396
- return createHash2("sha256").update(combined).digest("hex");
397
+ var init_digest = __esm({
398
+ "src/utils/digest.ts"() {
399
+ "use strict";
400
+ }
401
+ });
402
+
403
+ // src/commands/context-scan.ts
404
+ var context_scan_exports = {};
405
+ __export(context_scan_exports, {
406
+ contextScan: () => contextScan
407
+ });
408
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync7, readdirSync as readdirSync4, writeFileSync as writeFileSync3 } from "fs";
409
+ import { createHash as createHash3 } from "crypto";
410
+ import { basename, dirname as dirname3, join as join8, relative as relative2 } from "path";
411
+ function inputsDigest(paths, cwd) {
412
+ const combined = paths.slice().sort().map((p) => {
413
+ const rel = relative2(cwd, p).replace(/\\/g, "/");
414
+ return `${rel}:${sha256OfFileNormalized(p)}`;
415
+ }).join("\n");
416
+ return createHash3("sha256").update(combined).digest("hex");
397
417
  }
398
418
  function stripGlobSuffix(pattern) {
399
419
  return pattern.replace(/\/\*\*$/, "").replace(/\/\*$/, "");
@@ -403,7 +423,7 @@ function getForbiddenPaths(cwd) {
403
423
  const policyPath = join8(cwd, ".cdd", "context-policy.json");
404
424
  try {
405
425
  if (existsSync7(policyPath)) {
406
- const policy = JSON.parse(readFileSync6(policyPath, "utf8"));
426
+ const policy = JSON.parse(readFileSync7(policyPath, "utf8"));
407
427
  for (const pattern of policy.forbiddenPaths ?? []) {
408
428
  forbidden.add(stripGlobSuffix(pattern));
409
429
  }
@@ -549,7 +569,7 @@ async function contextScan(opts = {}) {
549
569
  `visible-files: ${treeStats.files}`,
550
570
  `omitted-dirs: ${treeStats.omittedDirs}`,
551
571
  `truncated-dirs: ${treeStats.truncatedDirs}`,
552
- `inputs-digest: ${inputsDigest(projectMapInputs)}`,
572
+ `inputs-digest: ${inputsDigest(projectMapInputs, cwd)}`,
553
573
  "---",
554
574
  "",
555
575
  "# Project Map",
@@ -577,7 +597,7 @@ async function contextScan(opts = {}) {
577
597
  for (const file of contractFiles) {
578
598
  const relPath = relative2(cwd, file).replace(/\\/g, "/");
579
599
  const dir = dirname3(relPath).replace(/\\/g, "/");
580
- const { title, summary, metadata } = parseContractMetadata(readFileSync6(file, "utf8"));
600
+ const { title, summary, metadata } = parseContractMetadata(readFileSync7(file, "utf8"));
581
601
  const contractType = deriveContractType(relPath, metadata);
582
602
  const owner = metadata.owner ?? "unknown";
583
603
  const surface2 = metadata.surface ?? dir;
@@ -615,7 +635,7 @@ async function contextScan(opts = {}) {
615
635
  "schema-version: 1",
616
636
  `contract-count: ${contractFiles.length}`,
617
637
  `missing-summary-count: ${missingSummary}`,
618
- `inputs-digest: ${inputsDigest(contractFiles)}`,
638
+ `inputs-digest: ${inputsDigest(contractFiles, cwd)}`,
619
639
  "---",
620
640
  "",
621
641
  "# Contracts Index",
@@ -644,6 +664,7 @@ var init_context_scan = __esm({
644
664
  "src/commands/context-scan.ts"() {
645
665
  "use strict";
646
666
  init_logger();
667
+ init_digest();
647
668
  DEFAULT_FORBIDDEN = [
648
669
  ".claude",
649
670
  ".git",
@@ -661,9 +682,13 @@ var init_context_scan = __esm({
661
682
  ".cdd/runtime"
662
683
  ];
663
684
  FORBIDDEN_DIRECTORY_NAMES = /* @__PURE__ */ new Set([
664
- // Node — also caught by top-level prefix, but a nested
665
- // `frontend/node_modules` requires basename matching
685
+ // Node / generic build outputs — also in DEFAULT_FORBIDDEN at top level,
686
+ // but nested cases like `frontend/dist`, `apps/foo/build`, `packages/x/out`
687
+ // require basename matching.
666
688
  "node_modules",
689
+ "dist",
690
+ "build",
691
+ "out",
667
692
  // Python
668
693
  "__pycache__",
669
694
  ".pytest_cache",
@@ -4263,49 +4288,49 @@ var require_fast_uri = __commonJS({
4263
4288
  schemelessOptions.skipEscape = true;
4264
4289
  return serialize(resolved, schemelessOptions);
4265
4290
  }
4266
- function resolveComponent(base, relative6, options, skipNormalization) {
4291
+ function resolveComponent(base, relative9, options, skipNormalization) {
4267
4292
  const target = {};
4268
4293
  if (!skipNormalization) {
4269
4294
  base = parse3(serialize(base, options), options);
4270
- relative6 = parse3(serialize(relative6, options), options);
4295
+ relative9 = parse3(serialize(relative9, options), options);
4271
4296
  }
4272
4297
  options = options || {};
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;
4298
+ if (!options.tolerant && relative9.scheme) {
4299
+ target.scheme = relative9.scheme;
4300
+ target.userinfo = relative9.userinfo;
4301
+ target.host = relative9.host;
4302
+ target.port = relative9.port;
4303
+ target.path = removeDotSegments(relative9.path || "");
4304
+ target.query = relative9.query;
4280
4305
  } else {
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;
4306
+ if (relative9.userinfo !== void 0 || relative9.host !== void 0 || relative9.port !== void 0) {
4307
+ target.userinfo = relative9.userinfo;
4308
+ target.host = relative9.host;
4309
+ target.port = relative9.port;
4310
+ target.path = removeDotSegments(relative9.path || "");
4311
+ target.query = relative9.query;
4287
4312
  } else {
4288
- if (!relative6.path) {
4313
+ if (!relative9.path) {
4289
4314
  target.path = base.path;
4290
- if (relative6.query !== void 0) {
4291
- target.query = relative6.query;
4315
+ if (relative9.query !== void 0) {
4316
+ target.query = relative9.query;
4292
4317
  } else {
4293
4318
  target.query = base.query;
4294
4319
  }
4295
4320
  } else {
4296
- if (relative6.path[0] === "/") {
4297
- target.path = removeDotSegments(relative6.path);
4321
+ if (relative9.path[0] === "/") {
4322
+ target.path = removeDotSegments(relative9.path);
4298
4323
  } else {
4299
4324
  if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) {
4300
- target.path = "/" + relative6.path;
4325
+ target.path = "/" + relative9.path;
4301
4326
  } else if (!base.path) {
4302
- target.path = relative6.path;
4327
+ target.path = relative9.path;
4303
4328
  } else {
4304
- target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative6.path;
4329
+ target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative9.path;
4305
4330
  }
4306
4331
  target.path = removeDotSegments(target.path);
4307
4332
  }
4308
- target.query = relative6.query;
4333
+ target.query = relative9.query;
4309
4334
  }
4310
4335
  target.userinfo = base.userinfo;
4311
4336
  target.host = base.host;
@@ -4313,7 +4338,7 @@ var require_fast_uri = __commonJS({
4313
4338
  }
4314
4339
  target.scheme = base.scheme;
4315
4340
  }
4316
- target.fragment = relative6.fragment;
4341
+ target.fragment = relative9.fragment;
4317
4342
  return target;
4318
4343
  }
4319
4344
  function equal(uriA, uriB, options) {
@@ -7475,7 +7500,7 @@ var require_dist = __commonJS({
7475
7500
  });
7476
7501
 
7477
7502
  // src/code-map/config.ts
7478
- import { existsSync as existsSync10, readFileSync as readFileSync8 } from "fs";
7503
+ import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
7479
7504
  import { join as join11 } from "path";
7480
7505
  import { load as yamlLoad } from "js-yaml";
7481
7506
  function asStringArray(value, key, where) {
@@ -7502,7 +7527,7 @@ function loadCodeMapConfig(cwd) {
7502
7527
  }
7503
7528
  let text;
7504
7529
  try {
7505
- text = readFileSync8(filePath, "utf8");
7530
+ text = readFileSync9(filePath, "utf8");
7506
7531
  } catch (err) {
7507
7532
  throw new Error(`failed to read ${CONFIG_REL_PATH}: ${err.message}`);
7508
7533
  }
@@ -7625,1569 +7650,1605 @@ var init_include_exclude = __esm({
7625
7650
  }
7626
7651
  });
7627
7652
 
7628
- // src/code-map/freshness.ts
7629
- var freshness_exports = {};
7630
- __export(freshness_exports, {
7631
- checkCodeMapFreshness: () => checkCodeMapFreshness
7632
- });
7633
- import { existsSync as existsSync11, statSync as statSync3 } from "fs";
7634
- import { join as join13 } from "path";
7635
- function checkCodeMapFreshness(cwd, mapRel = ".cdd/code-map.yml", include, exclude) {
7636
- const mapPath = join13(cwd, mapRel);
7637
- let cfg;
7638
- try {
7639
- cfg = loadCodeMapConfig(cwd);
7640
- } catch (err) {
7641
- return {
7642
- status: "config-error",
7643
- staleFiles: [],
7644
- staleCount: 0,
7645
- mapPath,
7646
- configError: err.message
7647
- };
7653
+ // src/code-map/yaml-writer.ts
7654
+ function quoteScalar(s) {
7655
+ if (s.startsWith(".") || YAML_RESERVED.test(s) || s.includes(" ") || s === "" || s === "null" || s === "true" || s === "false") {
7656
+ return `'${s.replace(/'/g, "''")}'`;
7648
7657
  }
7649
- const includeFinal = [...cfg.include, ...include ?? []];
7650
- const excludeFinal = [...cfg.exclude, ...exclude ?? []];
7651
- const sourceFiles = walkRepo(cwd, { include: includeFinal, exclude: excludeFinal });
7652
- if (!existsSync11(mapPath)) {
7653
- if (sourceFiles.length === 0) {
7654
- return { status: "missing-greenfield", staleFiles: [], staleCount: 0, mapPath };
7658
+ return s;
7659
+ }
7660
+ function quotePath(p) {
7661
+ if (YAML_RESERVED.test(p) || p.includes(" ")) {
7662
+ return `'${p.replace(/'/g, "''")}'`;
7663
+ }
7664
+ return p;
7665
+ }
7666
+ function renderItems(items) {
7667
+ if (items.length === 0)
7668
+ return "[]";
7669
+ return `[${items.map(quoteScalar).join(", ")}]`;
7670
+ }
7671
+ function truncateDecorator(d, max = 80) {
7672
+ const cleaned = d.replace(/\r?\n/g, " ");
7673
+ return cleaned.length <= max ? cleaned : cleaned.slice(0, max) + "...";
7674
+ }
7675
+ function renderClass(c) {
7676
+ const lines = [];
7677
+ lines.push(` - name: ${c.name}`);
7678
+ lines.push(` lines: ${c.lines[0]}-${c.lines[1]}`);
7679
+ if (c.methods.length > 0) {
7680
+ lines.push(" methods:");
7681
+ for (const m of c.methods) {
7682
+ const asyncPrefix = m.async ? "async " : "";
7683
+ lines.push(` - { name: ${asyncPrefix}${m.name}, lines: ${m.lines[0]}-${m.lines[1]} }`);
7655
7684
  }
7656
- return { status: "missing-with-sources", staleFiles: [], staleCount: 0, mapPath };
7657
7685
  }
7658
- const mapMtime = statSync3(mapPath).mtimeMs;
7659
- const staleAll = [];
7660
- for (const absPath of sourceFiles) {
7661
- try {
7662
- const mtime = statSync3(absPath).mtimeMs;
7663
- if (mtime > mapMtime) {
7664
- const rel = absPath.replace(/\\/g, "/").replace(cwd.replace(/\\/g, "/") + "/", "");
7665
- staleAll.push(rel);
7686
+ return lines;
7687
+ }
7688
+ function renderFunction(f) {
7689
+ const asyncPrefix = f.async ? "async " : "";
7690
+ let comment = "";
7691
+ if (f.decorators.length > 0) {
7692
+ const truncated = truncateDecorator(f.decorators[0]);
7693
+ comment = ` # @${truncated}`;
7694
+ }
7695
+ return ` - { name: ${asyncPrefix}${f.name}, lines: ${f.lines[0]}-${f.lines[1]} }${comment}`;
7696
+ }
7697
+ function renderTypeDef(t) {
7698
+ const exportedSuffix = t.exported ? "" : " # local";
7699
+ return ` - { name: ${t.name}, lines: ${t.lines[0]}-${t.lines[1]} }${exportedSuffix}`;
7700
+ }
7701
+ function renderEnum(e) {
7702
+ const lines = [];
7703
+ const exportedSuffix = e.exported ? "" : " # local";
7704
+ lines.push(` - name: ${e.name}`);
7705
+ lines.push(` lines: ${e.lines[0]}-${e.lines[1]}${exportedSuffix}`);
7706
+ if (e.members.length > 0) {
7707
+ lines.push(` members: [${e.members.join(", ")}]`);
7708
+ }
7709
+ return lines;
7710
+ }
7711
+ function renderYaml(entries, opts) {
7712
+ const now = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "Z");
7713
+ const totalSrc = entries.reduce((s, e) => s + e.total_lines, 0);
7714
+ const bodyLines = [];
7715
+ for (let i = 0; i < entries.length; i++) {
7716
+ const e = entries[i];
7717
+ if (i > 0)
7718
+ bodyLines.push("");
7719
+ const pathKey = quotePath(e.path);
7720
+ bodyLines.push(`${pathKey}: # ${e.total_lines} lines`);
7721
+ if (e.imports.length > 0) {
7722
+ bodyLines.push(" imports:");
7723
+ for (const imp of e.imports) {
7724
+ const mod = quoteScalar(imp.module);
7725
+ bodyLines.push(` - { module: ${mod}, items: ${renderItems(imp.items)}, line: ${imp.line} }`);
7726
+ }
7727
+ }
7728
+ if (e.constants.length > 0) {
7729
+ bodyLines.push(" constants:");
7730
+ for (const c of e.constants) {
7731
+ bodyLines.push(` - { name: ${c.name}, line: ${c.line} }`);
7732
+ }
7733
+ }
7734
+ if (e.classes.length > 0) {
7735
+ bodyLines.push(" classes:");
7736
+ for (const c of e.classes) {
7737
+ bodyLines.push(...renderClass(c));
7738
+ }
7739
+ }
7740
+ if (e.functions.length > 0) {
7741
+ bodyLines.push(" functions:");
7742
+ for (const f of e.functions) {
7743
+ bodyLines.push(renderFunction(f));
7744
+ }
7745
+ }
7746
+ if (e.interfaces && e.interfaces.length > 0) {
7747
+ bodyLines.push(" interfaces:");
7748
+ for (const t of e.interfaces) {
7749
+ bodyLines.push(renderTypeDef(t));
7750
+ }
7751
+ }
7752
+ if (e.types && e.types.length > 0) {
7753
+ bodyLines.push(" types:");
7754
+ for (const t of e.types) {
7755
+ bodyLines.push(renderTypeDef(t));
7756
+ }
7757
+ }
7758
+ if (e.enums && e.enums.length > 0) {
7759
+ bodyLines.push(" enums:");
7760
+ for (const en of e.enums) {
7761
+ bodyLines.push(...renderEnum(en));
7666
7762
  }
7667
- } catch {
7668
7763
  }
7669
7764
  }
7670
- if (staleAll.length === 0) {
7671
- return { status: "ok", staleFiles: [], staleCount: 0, mapPath };
7765
+ const headerLineCount = opts.sourcesDigest ? 3 : 2;
7766
+ const mapLines = bodyLines.length + headerLineCount;
7767
+ const compression = totalSrc === 0 ? 0 : totalSrc / mapLines;
7768
+ const fileCount = entries.length;
7769
+ const header = [
7770
+ `# generated: ${now} by ${opts.generator}`,
7771
+ `# files: ${fileCount}, src-lines: ${totalSrc}, map-lines: ${mapLines}, compression: ${compression.toFixed(1)}x`
7772
+ ];
7773
+ if (opts.sourcesDigest) {
7774
+ header.push(`# sources-digest: ${opts.sourcesDigest}`);
7672
7775
  }
7673
- return {
7674
- status: "stale",
7675
- staleFiles: staleAll.slice(0, 5),
7676
- staleCount: staleAll.length,
7677
- mapPath
7678
- };
7776
+ return [...header, ...bodyLines].join("\n") + "\n";
7679
7777
  }
7680
- var init_freshness = __esm({
7681
- "src/code-map/freshness.ts"() {
7778
+ var YAML_RESERVED;
7779
+ var init_yaml_writer = __esm({
7780
+ "src/code-map/yaml-writer.ts"() {
7682
7781
  "use strict";
7683
- init_include_exclude();
7684
- init_config();
7782
+ YAML_RESERVED = /[:[\]{},&*!|>'"%@`#]/;
7685
7783
  }
7686
7784
  });
7687
7785
 
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 });
7786
+ // src/code-map/orchestrator.ts
7787
+ async function scanInProcess(scanner, absolutePaths, repoRoot) {
7788
+ const entries = [];
7789
+ const warnings = [];
7790
+ for (const absPath of absolutePaths) {
7791
+ try {
7792
+ const entry = await scanner.scan(absPath, repoRoot);
7793
+ if (entry === null) {
7794
+ const rel = absPath.replace(/\\/g, "/").replace(repoRoot.replace(/\\/g, "/") + "/", "");
7795
+ warnings.push({ path: rel, message: "parse error (scanner returned null)" });
7796
+ } else {
7797
+ entries.push(entry);
7798
+ }
7799
+ } catch (err) {
7800
+ const rel = absPath.replace(/\\/g, "/").replace(repoRoot.replace(/\\/g, "/") + "/", "");
7801
+ warnings.push({ path: rel, message: `IO error: ${err.message}` });
7802
+ }
7703
7803
  }
7704
- return backupDir2;
7804
+ return { entries, warnings };
7705
7805
  }
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");
7806
+ function bucketByExtension(files) {
7807
+ const out = {};
7808
+ for (const f of files) {
7809
+ const dot = f.lastIndexOf(".");
7810
+ const ext = dot >= 0 ? f.slice(dot).toLowerCase() : "";
7811
+ if (!out[ext])
7812
+ out[ext] = [];
7813
+ out[ext].push(f);
7814
+ }
7815
+ return out;
7734
7816
  }
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");
7782
- }
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();
7817
+ var init_orchestrator = __esm({
7818
+ "src/code-map/orchestrator.ts"() {
7819
+ "use strict";
7820
+ init_include_exclude();
7796
7821
  }
7797
- return out;
7822
+ });
7823
+
7824
+ // src/code-map/scanners/common.ts
7825
+ import { relative as relative4 } from "path";
7826
+ function canonicalRelPath(absolutePath, repoRoot) {
7827
+ const rel = relative4(repoRoot, absolutePath);
7828
+ return rel.replace(/\\/g, "/").normalize("NFC");
7798
7829
  }
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);
7830
+ function isAllCapsConst(name) {
7831
+ return name.length >= 2 && /^[A-Z][A-Z0-9_]*$/.test(name);
7807
7832
  }
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;
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 });
7830
- }
7831
- return rows;
7833
+ function isBinary(content) {
7834
+ const head = content.slice(0, 4096);
7835
+ return head.includes("\0");
7832
7836
  }
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;
7838
- }
7839
- if (!existsSync14(legacyPath)) {
7840
- warnings.push("tasks.md not found and tasks.yml missing \u2014 skipping tasks migration");
7841
- return;
7842
- }
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;
7837
+ var init_common = __esm({
7838
+ "src/code-map/scanners/common.ts"() {
7839
+ "use strict";
7857
7840
  }
7858
- if (enableContextGovernance || fm["context-governance"] === "v1") {
7859
- data["context-governance"] = "v1";
7841
+ });
7842
+
7843
+ // src/code-map/scanners/python.ts
7844
+ var python_exports = {};
7845
+ __export(python_exports, {
7846
+ pythonScanner: () => pythonScanner
7847
+ });
7848
+ import { spawnSync as spawnSync2 } from "child_process";
7849
+ import { writeFileSync as writeFileSync5, unlinkSync } from "fs";
7850
+ import { join as join13 } from "path";
7851
+ import { tmpdir } from "os";
7852
+ function detectPython2() {
7853
+ for (const candidate of ["python3", "python"]) {
7854
+ try {
7855
+ const result = spawnSync2(candidate, ["--version"], {
7856
+ encoding: "utf8",
7857
+ timeout: 5e3
7858
+ });
7859
+ if (result.status === 0)
7860
+ return candidate;
7861
+ } catch {
7862
+ }
7860
7863
  }
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)`);
7864
+ return null;
7875
7865
  }
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());
7866
+ var TIMEOUT_MS, PythonScanner, pythonScanner;
7867
+ var init_python = __esm({
7868
+ "src/code-map/scanners/python.ts"() {
7869
+ "use strict";
7870
+ init_paths();
7871
+ init_common();
7872
+ TIMEOUT_MS = parseInt(process.env["CDD_CODE_MAP_TIMEOUT_MS"] ?? "30000", 10);
7873
+ PythonScanner = class {
7874
+ extensions = [".py"];
7875
+ _interpreter = void 0;
7876
+ getInterpreter() {
7877
+ if (this._interpreter === void 0) {
7878
+ this._interpreter = detectPython2();
7902
7879
  }
7903
- j++;
7880
+ return this._interpreter;
7904
7881
  }
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 };
7912
- }
7913
- const type = s.slice(0, idx).trim();
7914
- const pointer = s.slice(idx + 1).trim();
7915
- return { type, pointer };
7916
- });
7882
+ async scan(absolutePath, repoRoot) {
7883
+ const result = await this.scanBatch([absolutePath], repoRoot);
7884
+ return result.entries[0] ?? null;
7917
7885
  }
7918
- i = j;
7919
- continue;
7920
- }
7921
- data[key] = inline;
7922
- i++;
7923
- }
7924
- return data;
7925
- }
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
- }
7944
- }
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)`
7886
+ async scanBatch(absolutePaths, repoRoot) {
7887
+ const interpreter = this.getInterpreter();
7888
+ const entries = [];
7889
+ const warnings = [];
7890
+ if (!interpreter) {
7891
+ const count = absolutePaths.length;
7892
+ warnings.push({
7893
+ path: "",
7894
+ message: `python interpreter not found on PATH; skipping ${count} .py file${count === 1 ? "" : "s"}`
7895
+ });
7896
+ return { entries, warnings };
7897
+ }
7898
+ const scriptPath = ASSET.codeMapPython;
7899
+ const rand = Math.random().toString(36).slice(2);
7900
+ const listFile = join13(tmpdir(), `cdd-codemap-${process.pid}-${rand}.txt`);
7901
+ writeFileSync5(listFile, absolutePaths.join("\n") + "\n", "utf8");
7902
+ let stdout = "";
7903
+ let stderr = "";
7904
+ let exitCode = 0;
7905
+ try {
7906
+ const result = spawnSync2(
7907
+ interpreter,
7908
+ [scriptPath, "--batch-file", listFile, "--repo-root", repoRoot],
7909
+ {
7910
+ encoding: "utf8",
7911
+ timeout: TIMEOUT_MS,
7912
+ maxBuffer: 50 * 1024 * 1024
7913
+ // 50MB
7914
+ }
7967
7915
  );
7968
- pending.push({ path: classifPath, content: content + addition });
7916
+ stdout = result.stdout ?? "";
7917
+ stderr = result.stderr ?? "";
7918
+ exitCode = result.status ?? -1;
7919
+ if (result.error) {
7920
+ const errMsg = result.error.message ?? String(result.error);
7921
+ if (errMsg.includes("ENOENT")) {
7922
+ warnings.push({
7923
+ path: "",
7924
+ message: `python interpreter not found (ENOENT); skipping ${absolutePaths.length} .py file(s)`
7925
+ });
7926
+ return { entries, warnings };
7927
+ }
7928
+ if (errMsg.includes("ETIMEDOUT") || errMsg.includes("timeout")) {
7929
+ warnings.push({
7930
+ path: "",
7931
+ message: `python scanner timed out after ${TIMEOUT_MS}ms; skipping .py files`
7932
+ });
7933
+ return { entries, warnings };
7934
+ }
7935
+ warnings.push({
7936
+ path: "",
7937
+ message: `python scanner error: ${errMsg}; skipping .py files`
7938
+ });
7939
+ return { entries, warnings };
7940
+ }
7941
+ } finally {
7942
+ try {
7943
+ unlinkSync(listFile);
7944
+ } catch {
7945
+ }
7969
7946
  }
7970
- } else {
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
- );
7947
+ if (exitCode === 3) {
7948
+ warnings.push({
7949
+ path: "",
7950
+ message: "python interpreter is < 3.9 (need ast.unparse); skipping .py files"
7951
+ });
7952
+ return { entries, warnings };
7953
+ }
7954
+ for (const line of stdout.split("\n")) {
7955
+ const trimmed = line.trim();
7956
+ if (!trimmed)
7957
+ continue;
7958
+ let parsed;
7959
+ try {
7960
+ parsed = JSON.parse(trimmed);
7961
+ } catch {
7962
+ continue;
7963
+ }
7964
+ if (!parsed.ok) {
7965
+ warnings.push({ path: parsed.path, message: parsed.error });
7966
+ continue;
7967
+ }
7968
+ const r = parsed;
7969
+ entries.push({
7970
+ path: canonicalRelPath(join13(repoRoot, r.path), repoRoot),
7971
+ total_lines: r.total_lines,
7972
+ imports: r.imports ?? [],
7973
+ constants: r.constants ?? [],
7974
+ classes: (r.classes ?? []).map((c) => ({
7975
+ name: c.name,
7976
+ lines: [c.lines[0], c.lines[1]],
7977
+ methods: (c.methods ?? []).map((m) => ({
7978
+ name: m.name,
7979
+ lines: [m.lines[0], m.lines[1]],
7980
+ async: m.async
7981
+ }))
7982
+ })),
7983
+ functions: (r.functions ?? []).map((f) => ({
7984
+ name: f.name,
7985
+ lines: [f.lines[0], f.lines[1]],
7986
+ decorators: f.decorators ?? [],
7987
+ async: f.async
7988
+ }))
7989
+ });
7990
+ }
7991
+ if (exitCode === 2) {
7992
+ warnings.push({
7993
+ path: "",
7994
+ message: `python scanner exited with code 2 (fatal); partial results only. stderr: ${stderr.slice(0, 200)}`
7995
+ });
7996
+ }
7997
+ return { entries, warnings };
7974
7998
  }
7975
- } else {
7976
- const structured = content.match(/^## Tier\s*\n\s*-\s*(\d)\s*$/m);
7977
- if (structured)
7978
- detectedTier = structured[1];
7979
- }
7980
- }
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");
7999
+ };
8000
+ pythonScanner = new PythonScanner();
7992
8001
  }
7993
- return { result: { changed, warnings }, pending, deletes };
8002
+ });
8003
+
8004
+ // src/code-map/scanners/javascript.ts
8005
+ var javascript_exports = {};
8006
+ __export(javascript_exports, {
8007
+ COMMON_PLUGINS: () => COMMON_PLUGINS,
8008
+ countSourceLines: () => countSourceLines,
8009
+ jsScanner: () => jsScanner,
8010
+ parseAndExtract: () => parseAndExtract,
8011
+ parseJsSource: () => parseJsSource,
8012
+ parseSourceWithPlugins: () => parseSourceWithPlugins
8013
+ });
8014
+ import { readFileSync as readFileSync11 } from "fs";
8015
+ import { parse } from "@babel/parser";
8016
+ function parseSourceWithPlugins(source, plugins) {
8017
+ return parse(source, {
8018
+ sourceType: "unambiguous",
8019
+ allowReturnOutsideFunction: true,
8020
+ allowAwaitOutsideFunction: true,
8021
+ errorRecovery: true,
8022
+ plugins
8023
+ });
7994
8024
  }
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 {
8008
- }
8025
+ function isCallExpression(node, callee) {
8026
+ if (!node || node.type !== "CallExpression")
8027
+ return false;
8028
+ const c = node.callee;
8029
+ return c.type === "Identifier" && c.name === callee;
8030
+ }
8031
+ function extractRequireModule(node) {
8032
+ if (!node || node.type !== "CallExpression")
8033
+ return null;
8034
+ const c = node.callee;
8035
+ if (c.type !== "Identifier" || c.name !== "require")
8036
+ return null;
8037
+ const arg = node.arguments[0];
8038
+ if (!arg || arg.type !== "StringLiteral")
8039
+ return null;
8040
+ return arg.value;
8041
+ }
8042
+ function getLineRange(node) {
8043
+ const start = node.loc?.start.line ?? 1;
8044
+ const end = node.loc?.end.line ?? start;
8045
+ return [start, end];
8046
+ }
8047
+ function processImportDeclaration(node) {
8048
+ const items = [];
8049
+ for (const spec of node.specifiers) {
8050
+ if (spec.type === "ImportDefaultSpecifier") {
8051
+ items.push(`default:${spec.local.name}`);
8052
+ } else if (spec.type === "ImportNamespaceSpecifier") {
8053
+ items.push(`*:${spec.local.name}`);
8054
+ } else if (spec.type === "ImportSpecifier") {
8055
+ const imported = spec.imported;
8056
+ items.push(imported.type === "Identifier" ? imported.name : spec.local.name);
8009
8057
  }
8010
- throw err;
8011
- }
8012
- for (const r of renames) {
8013
- renameSync(r.tmp, r.final);
8014
8058
  }
8015
- for (const d of deletes) {
8016
- try {
8017
- rmSync2(d.path, { force: true });
8018
- } catch {
8059
+ return {
8060
+ module: node.source.value,
8061
+ items,
8062
+ line: node.loc?.start.line ?? 1
8063
+ };
8064
+ }
8065
+ function processFunctionDeclaration(node, nameOverride) {
8066
+ const name = nameOverride ?? node.id?.name;
8067
+ if (!name)
8068
+ return null;
8069
+ return {
8070
+ name,
8071
+ lines: getLineRange(node),
8072
+ decorators: [],
8073
+ async: node.async
8074
+ };
8075
+ }
8076
+ function processClassDeclaration(node, nameOverride) {
8077
+ const name = nameOverride ?? node.id?.name;
8078
+ if (!name)
8079
+ return null;
8080
+ const methods = [];
8081
+ for (const member of node.body.body) {
8082
+ if (member.type === "ClassMethod" || member.type === "ClassPrivateMethod") {
8083
+ const m = member;
8084
+ let methodName;
8085
+ if (m.key.type === "Identifier") {
8086
+ methodName = m.key.name;
8087
+ } else if (m.key.type === "PrivateName") {
8088
+ methodName = `#${m.key.id.name}`;
8089
+ } else {
8090
+ methodName = "<computed>";
8091
+ }
8092
+ methods.push({
8093
+ name: methodName,
8094
+ lines: getLineRange(m),
8095
+ async: m.async
8096
+ });
8019
8097
  }
8020
8098
  }
8099
+ return {
8100
+ name,
8101
+ lines: getLineRange(node),
8102
+ methods
8103
+ };
8021
8104
  }
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`);
8105
+ function processVariableDeclaration(node, imports, constants, functions) {
8106
+ for (const decl of node.declarations) {
8107
+ if (!decl.id || decl.id.type !== "Identifier")
8064
8108
  continue;
8065
- }
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}`);
8109
+ const varName = decl.id.name;
8110
+ const init2 = decl.init;
8111
+ if (init2 && isCallExpression(init2, "require")) {
8112
+ const mod = extractRequireModule(init2);
8113
+ if (mod) {
8114
+ imports.push({ module: mod, items: [`default:${varName}`], line: node.loc?.start.line ?? 1 });
8115
+ }
8073
8116
  continue;
8074
8117
  }
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
- }
8118
+ if (isAllCapsConst(varName) && init2 !== null && init2 !== void 0) {
8119
+ constants.push({ name: varName, line: node.loc?.start.line ?? 1 });
8120
+ continue;
8087
8121
  }
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/");
8122
+ if (init2 && (init2.type === "ArrowFunctionExpression" || init2.type === "FunctionExpression")) {
8123
+ functions.push({
8124
+ name: varName,
8125
+ lines: getLineRange(node),
8126
+ decorators: [],
8127
+ async: init2.async
8128
+ });
8129
+ } else if (init2 && init2.type === "CallExpression" && /^[A-Z]/.test(varName)) {
8130
+ functions.push({
8131
+ name: varName,
8132
+ lines: getLineRange(node),
8133
+ decorators: [],
8134
+ async: false
8135
+ });
8107
8136
  }
8108
8137
  }
8109
8138
  }
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;
8139
+ function processTsInterfaceDeclaration(node, exported) {
8140
+ if (!node || node.type !== "TSInterfaceDeclaration")
8141
+ return null;
8142
+ if (!node.id || node.id.type !== "Identifier")
8143
+ return null;
8144
+ return {
8145
+ name: node.id.name,
8146
+ lines: getLineRange(node),
8147
+ exported
8148
+ };
8130
8149
  }
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);
8150
+ function processTsTypeAliasDeclaration(node, exported) {
8151
+ if (!node || node.type !== "TSTypeAliasDeclaration")
8152
+ return null;
8153
+ if (!node.id || node.id.type !== "Identifier")
8154
+ return null;
8155
+ return {
8156
+ name: node.id.name,
8157
+ lines: getLineRange(node),
8158
+ exported
8159
+ };
8160
+ }
8161
+ function processTsEnumDeclaration(node, exported) {
8162
+ if (!node || node.type !== "TSEnumDeclaration")
8163
+ return null;
8164
+ if (!node.id || node.id.type !== "Identifier")
8165
+ return null;
8166
+ const members = [];
8167
+ for (const m of node.members ?? []) {
8168
+ if (!m || !m.id)
8153
8169
  continue;
8154
- }
8155
- if (!existsSync15(dest)) {
8156
- planned.push({ src, dest, rel: join17(label, relative3(srcDir, src)) });
8157
- }
8170
+ if (m.id.type === "Identifier")
8171
+ members.push(m.id.name);
8172
+ else if (m.id.type === "StringLiteral")
8173
+ members.push(m.id.value);
8158
8174
  }
8175
+ return {
8176
+ name: node.id.name,
8177
+ lines: getLineRange(node),
8178
+ exported,
8179
+ members
8180
+ };
8159
8181
  }
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
- }
8182
+ function processStatement(stmt, buckets, extractTsTypes, exportedFromWrapper = false) {
8183
+ if (stmt.type === "ExportNamedDeclaration" && stmt.declaration) {
8184
+ processStatement(stmt.declaration, buckets, extractTsTypes, true);
8185
+ return;
8168
8186
  }
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" });
8187
+ if (stmt.type === "ExportDefaultDeclaration") {
8188
+ const decl = stmt.declaration;
8189
+ if (decl.type === "FunctionDeclaration") {
8190
+ const fe = processFunctionDeclaration(decl, decl.id?.name ?? "default");
8191
+ if (fe)
8192
+ buckets.functions.push(fe);
8193
+ } else if (decl.type === "ClassDeclaration") {
8194
+ const ce = processClassDeclaration(decl, decl.id?.name ?? "default");
8195
+ if (ce)
8196
+ buckets.classes.push(ce);
8197
+ }
8198
+ return;
8171
8199
  }
8172
- }
8173
- function applyCopy(plan) {
8174
- for (const item of plan) {
8175
- mkdirSync7(dirname4(item.dest), { recursive: true });
8176
- copyFileSync3(item.src, item.dest);
8200
+ if (stmt.type === "ImportDeclaration") {
8201
+ buckets.imports.push(processImportDeclaration(stmt));
8202
+ return;
8177
8203
  }
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);
8204
+ if (stmt.type === "FunctionDeclaration") {
8205
+ const fe = processFunctionDeclaration(stmt);
8206
+ if (fe)
8207
+ buckets.functions.push(fe);
8208
+ return;
8185
8209
  }
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
- });
8207
- }
8208
- log.blank();
8210
+ if (stmt.type === "ClassDeclaration") {
8211
+ const ce = processClassDeclaration(stmt);
8212
+ if (ce)
8213
+ buckets.classes.push(ce);
8209
8214
  return;
8210
8215
  }
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();
8216
+ if (stmt.type === "VariableDeclaration") {
8217
+ processVariableDeclaration(stmt, buckets.imports, buckets.constants, buckets.functions);
8227
8218
  return;
8228
8219
  }
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");
8244
- }
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
- });
8256
- }
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();
8266
- }
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]} }`);
8300
- }
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
- }
8343
- }
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
- }
8220
+ if (extractTsTypes) {
8221
+ const anyStmt = stmt;
8222
+ if (anyStmt.type === "TSInterfaceDeclaration") {
8223
+ const e = processTsInterfaceDeclaration(anyStmt, exportedFromWrapper);
8224
+ if (e)
8225
+ buckets.interfaces.push(e);
8226
+ return;
8367
8227
  }
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
- }
8228
+ if (anyStmt.type === "TSTypeAliasDeclaration") {
8229
+ const e = processTsTypeAliasDeclaration(anyStmt, exportedFromWrapper);
8230
+ if (e)
8231
+ buckets.types.push(e);
8232
+ return;
8373
8233
  }
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
- }
8234
+ if (anyStmt.type === "TSEnumDeclaration") {
8235
+ const e = processTsEnumDeclaration(anyStmt, exportedFromWrapper);
8236
+ if (e)
8237
+ buckets.enums.push(e);
8238
+ return;
8379
8239
  }
8380
8240
  }
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);
8241
+ if (stmt.type === "ExpressionStatement") {
8242
+ const expr = stmt.expression;
8243
+ if (isCallExpression(expr, "require")) {
8244
+ const mod = extractRequireModule(expr);
8245
+ if (mod) {
8246
+ buckets.imports.push({ module: mod, items: [], line: stmt.loc?.start.line ?? 1 });
8410
8247
  }
8411
- } catch (err) {
8412
- const rel = absPath.replace(/\\/g, "/").replace(repoRoot.replace(/\\/g, "/") + "/", "");
8413
- warnings.push({ path: rel, message: `IO error: ${err.message}` });
8414
8248
  }
8415
8249
  }
8416
- return { entries, warnings };
8417
8250
  }
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);
8251
+ function parseAndExtract(source, opts) {
8252
+ let ast;
8253
+ try {
8254
+ ast = parseSourceWithPlugins(source, opts.plugins);
8255
+ } catch {
8256
+ return null;
8426
8257
  }
8427
- return out;
8428
- }
8429
- var init_orchestrator = __esm({
8430
- "src/code-map/orchestrator.ts"() {
8431
- "use strict";
8432
- init_include_exclude();
8258
+ const buckets = {
8259
+ imports: [],
8260
+ constants: [],
8261
+ functions: [],
8262
+ classes: [],
8263
+ interfaces: [],
8264
+ types: [],
8265
+ enums: []
8266
+ };
8267
+ for (const stmt of ast.program.body) {
8268
+ processStatement(stmt, buckets, !!opts.extractTsTypes);
8433
8269
  }
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");
8270
+ return buckets;
8441
8271
  }
8442
- function isAllCapsConst(name) {
8443
- return name.length >= 2 && /^[A-Z][A-Z0-9_]*$/.test(name);
8272
+ function countSourceLines(source) {
8273
+ if (source === "")
8274
+ return 0;
8275
+ return source.split(/\r?\n/).length - (source.endsWith("\n") || source.endsWith("\r\n") ? 1 : 0);
8444
8276
  }
8445
- function isBinary(content) {
8446
- const head = content.slice(0, 4096);
8447
- return head.includes("\0");
8277
+ function parseJsSource(source, relPath) {
8278
+ const total_lines = countSourceLines(source);
8279
+ const r = parseAndExtract(source, { plugins: JS_PLUGINS, extractTsTypes: false });
8280
+ if (!r)
8281
+ return null;
8282
+ return {
8283
+ path: relPath,
8284
+ total_lines,
8285
+ imports: r.imports,
8286
+ constants: r.constants,
8287
+ classes: r.classes,
8288
+ functions: r.functions
8289
+ };
8448
8290
  }
8449
- var init_common = __esm({
8450
- "src/code-map/scanners/common.ts"() {
8451
- "use strict";
8452
- }
8291
+ var COMMON_PLUGINS, JS_PLUGINS, JavaScriptScanner, jsScanner;
8292
+ var init_javascript = __esm({
8293
+ "src/code-map/scanners/javascript.ts"() {
8294
+ "use strict";
8295
+ init_common();
8296
+ COMMON_PLUGINS = [
8297
+ "classProperties",
8298
+ "classPrivateProperties",
8299
+ "classPrivateMethods",
8300
+ "decorators-legacy",
8301
+ "topLevelAwait",
8302
+ "dynamicImport",
8303
+ "optionalChaining",
8304
+ "nullishCoalescingOperator",
8305
+ "objectRestSpread",
8306
+ "asyncGenerators",
8307
+ "numericSeparator",
8308
+ "logicalAssignment"
8309
+ ];
8310
+ JS_PLUGINS = ["jsx", ...COMMON_PLUGINS];
8311
+ JavaScriptScanner = class {
8312
+ extensions = [".js"];
8313
+ async scan(absolutePath, repoRoot) {
8314
+ let source;
8315
+ try {
8316
+ source = readFileSync11(absolutePath, "utf8");
8317
+ } catch (err) {
8318
+ throw err;
8319
+ }
8320
+ if (isBinary(source)) {
8321
+ return null;
8322
+ }
8323
+ const relPath = canonicalRelPath(absolutePath, repoRoot);
8324
+ return parseJsSource(source, relPath);
8325
+ }
8326
+ };
8327
+ jsScanner = new JavaScriptScanner();
8328
+ }
8453
8329
  });
8454
8330
 
8455
- // src/code-map/scanners/python.ts
8456
- var python_exports = {};
8457
- __export(python_exports, {
8458
- pythonScanner: () => pythonScanner
8331
+ // src/code-map/scanners/typescript.ts
8332
+ var typescript_exports = {};
8333
+ __export(typescript_exports, {
8334
+ tsScanner: () => tsScanner
8459
8335
  });
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"() {
8336
+ import { readFileSync as readFileSync12 } from "fs";
8337
+ var TypeScriptScanner, tsScanner;
8338
+ var init_typescript = __esm({
8339
+ "src/code-map/scanners/typescript.ts"() {
8481
8340
  "use strict";
8482
- init_paths();
8483
8341
  init_common();
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();
8342
+ init_javascript();
8343
+ TypeScriptScanner = class {
8344
+ extensions = [".ts", ".tsx"];
8345
+ async scan(absolutePath, repoRoot) {
8346
+ let source;
8347
+ try {
8348
+ source = readFileSync12(absolutePath, "utf8");
8349
+ } catch (err) {
8350
+ throw err;
8491
8351
  }
8492
- return this._interpreter;
8352
+ if (isBinary(source)) {
8353
+ return null;
8354
+ }
8355
+ const isTsx = absolutePath.toLowerCase().endsWith(".tsx");
8356
+ const plugins = isTsx ? ["typescript", "jsx", ...COMMON_PLUGINS] : ["typescript", ...COMMON_PLUGINS];
8357
+ const r = parseAndExtract(source, { plugins, extractTsTypes: true });
8358
+ if (!r)
8359
+ return null;
8360
+ const relPath = canonicalRelPath(absolutePath, repoRoot);
8361
+ const total_lines = countSourceLines(source);
8362
+ return {
8363
+ path: relPath,
8364
+ total_lines,
8365
+ imports: r.imports,
8366
+ constants: r.constants,
8367
+ classes: r.classes,
8368
+ functions: r.functions,
8369
+ interfaces: r.interfaces,
8370
+ types: r.types,
8371
+ enums: r.enums
8372
+ };
8493
8373
  }
8374
+ };
8375
+ tsScanner = new TypeScriptScanner();
8376
+ }
8377
+ });
8378
+
8379
+ // src/code-map/scanners/vue.ts
8380
+ var vue_exports = {};
8381
+ __export(vue_exports, {
8382
+ vueScanner: () => vueScanner
8383
+ });
8384
+ import { readFileSync as readFileSync13 } from "fs";
8385
+ import { parse as parse2 } from "@vue/compiler-sfc";
8386
+ var VueScanner, vueScanner;
8387
+ var init_vue = __esm({
8388
+ "src/code-map/scanners/vue.ts"() {
8389
+ "use strict";
8390
+ init_common();
8391
+ init_javascript();
8392
+ VueScanner = class {
8393
+ extensions = [".vue"];
8494
8394
  async scan(absolutePath, repoRoot) {
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 = [];
8501
- const warnings = [];
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 };
8509
- }
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;
8395
+ let source;
8517
8396
  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
- }
8547
- warnings.push({
8548
- path: "",
8549
- message: `python scanner error: ${errMsg}; skipping .py files`
8550
- });
8551
- return { entries, warnings };
8552
- }
8553
- } finally {
8554
- try {
8555
- unlinkSync(listFile);
8556
- } catch {
8557
- }
8397
+ source = readFileSync13(absolutePath, "utf8");
8398
+ } catch (err) {
8399
+ throw err;
8558
8400
  }
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 };
8401
+ if (isBinary(source)) {
8402
+ return null;
8565
8403
  }
8566
- for (const line of stdout.split("\n")) {
8567
- const trimmed = line.trim();
8568
- if (!trimmed)
8404
+ const relPath = canonicalRelPath(absolutePath, repoRoot);
8405
+ const total_lines = source.split(/\r?\n/).length - (source.endsWith("\n") || source.endsWith("\r\n") ? 1 : 0);
8406
+ const { descriptor } = parse2(source, { filename: absolutePath });
8407
+ const allImports = [];
8408
+ const allConstants = [];
8409
+ const allFunctions = [];
8410
+ const allClasses = [];
8411
+ const warnings = [];
8412
+ const scriptBlocks = [descriptor.script, descriptor.scriptSetup].filter(Boolean);
8413
+ if (scriptBlocks.length === 0) {
8414
+ return {
8415
+ path: relPath,
8416
+ total_lines,
8417
+ imports: [],
8418
+ constants: [],
8419
+ classes: [],
8420
+ functions: []
8421
+ };
8422
+ }
8423
+ for (const block of scriptBlocks) {
8424
+ if (!block)
8569
8425
  continue;
8570
- let parsed;
8571
- try {
8572
- parsed = JSON.parse(trimmed);
8573
- } catch {
8426
+ if (block.lang === "ts" || block.lang === "tsx") {
8427
+ warnings.push({
8428
+ path: relPath,
8429
+ message: `skipping <script lang=${block.lang}> block (TypeScript not supported in v1)`
8430
+ });
8574
8431
  continue;
8575
8432
  }
8576
- if (!parsed.ok) {
8577
- warnings.push({ path: parsed.path, message: parsed.error });
8433
+ const blockContent = block.content;
8434
+ const lineOffset = block.loc.start.line;
8435
+ const scriptEntry = parseJsSource(blockContent, relPath);
8436
+ if (!scriptEntry) {
8437
+ warnings.push({ path: relPath, message: "parse error in script block" });
8578
8438
  continue;
8579
8439
  }
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
8593
- }))
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
- });
8602
- }
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
- });
8440
+ const offset = lineOffset - 1;
8441
+ for (const imp of scriptEntry.imports) {
8442
+ allImports.push({ ...imp, line: imp.line + offset });
8443
+ }
8444
+ for (const c of scriptEntry.constants) {
8445
+ allConstants.push({ ...c, line: c.line + offset });
8446
+ }
8447
+ for (const fn of scriptEntry.functions) {
8448
+ allFunctions.push({
8449
+ ...fn,
8450
+ lines: [fn.lines[0] + offset, fn.lines[1] + offset]
8451
+ });
8452
+ }
8453
+ for (const cls of scriptEntry.classes) {
8454
+ allClasses.push({
8455
+ ...cls,
8456
+ lines: [cls.lines[0] + offset, cls.lines[1] + offset],
8457
+ methods: cls.methods.map((m) => ({
8458
+ ...m,
8459
+ lines: [m.lines[0] + offset, m.lines[1] + offset]
8460
+ }))
8461
+ });
8462
+ }
8608
8463
  }
8609
- return { entries, warnings };
8464
+ return {
8465
+ path: relPath,
8466
+ total_lines,
8467
+ imports: allImports,
8468
+ constants: allConstants,
8469
+ classes: allClasses,
8470
+ functions: allFunctions
8471
+ };
8610
8472
  }
8611
8473
  };
8612
- pythonScanner = new PythonScanner();
8474
+ vueScanner = new VueScanner();
8613
8475
  }
8614
8476
  });
8615
8477
 
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
8478
+ // src/commands/code-map.ts
8479
+ var code_map_exports = {};
8480
+ __export(code_map_exports, {
8481
+ codeMap: () => codeMap,
8482
+ computeSourcesDigest: () => computeSourcesDigest
8625
8483
  });
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
8484
+ import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync14, writeFileSync as writeFileSync6 } from "fs";
8485
+ import { resolve, dirname as dirname4, relative as relative5 } from "path";
8486
+ import { createHash as createHash5 } from "crypto";
8487
+ import { createRequire } from "module";
8488
+ import { fileURLToPath as fileURLToPath2 } from "url";
8489
+ import { join as join14 } from "path";
8490
+ function computeSourcesDigest(absolutePaths, cwd) {
8491
+ const lines = absolutePaths.slice().sort().map((p) => {
8492
+ const rel = relative5(cwd, p).replace(/\\/g, "/");
8493
+ const contentHash = sha256OfFileNormalized(p) || "missing";
8494
+ return `${rel}:${contentHash}`;
8635
8495
  });
8496
+ return createHash5("sha256").update(lines.join("\n")).digest("hex");
8636
8497
  }
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;
8642
- }
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;
8653
- }
8654
- function getLineRange(node) {
8655
- const start = node.loc?.start.line ?? 1;
8656
- const end = node.loc?.end.line ?? start;
8657
- return [start, end];
8658
- }
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);
8498
+ async function codeMap(opts) {
8499
+ const root = resolve(process.cwd(), opts.path);
8500
+ const start = Date.now();
8501
+ let cfg;
8502
+ try {
8503
+ cfg = loadCodeMapConfig(root);
8504
+ } catch (err) {
8505
+ log.error(`code-map: ${err.message}`);
8506
+ return 1;
8507
+ }
8508
+ const include = [...cfg.include, ...opts.include];
8509
+ const exclude = [...cfg.exclude, ...opts.exclude];
8510
+ const files = walkRepo(root, { include, exclude });
8511
+ const buckets = bucketByExtension(files);
8512
+ const result = { entries: [], warnings: [] };
8513
+ const tasks = [];
8514
+ if (buckets[".py"]?.length) {
8515
+ const { pythonScanner: pythonScanner2 } = await Promise.resolve().then(() => (init_python(), python_exports));
8516
+ if (pythonScanner2.scanBatch) {
8517
+ tasks.push(pythonScanner2.scanBatch(buckets[".py"], root));
8669
8518
  }
8670
8519
  }
8671
- return {
8672
- module: node.source.value,
8673
- items,
8674
- line: node.loc?.start.line ?? 1
8675
- };
8676
- }
8677
- function processFunctionDeclaration(node, nameOverride) {
8678
- const name = nameOverride ?? node.id?.name;
8679
- if (!name)
8680
- return null;
8681
- return {
8682
- name,
8683
- lines: getLineRange(node),
8684
- decorators: [],
8685
- async: node.async
8686
- };
8687
- }
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>";
8703
- }
8704
- methods.push({
8705
- name: methodName,
8706
- lines: getLineRange(m),
8707
- async: m.async
8520
+ const jsFiles = [
8521
+ ...buckets[".js"] ?? [],
8522
+ ...buckets[".jsx"] ?? [],
8523
+ ...buckets[".mjs"] ?? [],
8524
+ ...buckets[".cjs"] ?? []
8525
+ ];
8526
+ if (jsFiles.length) {
8527
+ const { jsScanner: jsScanner2 } = await Promise.resolve().then(() => (init_javascript(), javascript_exports));
8528
+ tasks.push(scanInProcess(jsScanner2, jsFiles, root));
8529
+ }
8530
+ const tsFiles = [
8531
+ ...buckets[".ts"] ?? [],
8532
+ ...buckets[".tsx"] ?? []
8533
+ ];
8534
+ if (tsFiles.length) {
8535
+ const { tsScanner: tsScanner2 } = await Promise.resolve().then(() => (init_typescript(), typescript_exports));
8536
+ tasks.push(scanInProcess(tsScanner2, tsFiles, root));
8537
+ }
8538
+ if (buckets[".vue"]?.length) {
8539
+ const { vueScanner: vueScanner2 } = await Promise.resolve().then(() => (init_vue(), vue_exports));
8540
+ tasks.push(scanInProcess(vueScanner2, buckets[".vue"], root));
8541
+ }
8542
+ for (const r of await Promise.all(tasks)) {
8543
+ result.entries.push(...r.entries);
8544
+ result.warnings.push(...r.warnings);
8545
+ }
8546
+ result.entries.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
8547
+ for (const e of result.entries) {
8548
+ if (e.total_lines > opts.maxLines) {
8549
+ result.warnings.push({
8550
+ path: e.path,
8551
+ message: `file exceeds --max-lines (${e.total_lines} > ${opts.maxLines})`
8708
8552
  });
8709
8553
  }
8710
8554
  }
8711
- return {
8712
- name,
8713
- lines: getLineRange(node),
8714
- methods
8715
- };
8716
- }
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 });
8727
- }
8728
- continue;
8729
- }
8730
- if (isAllCapsConst(varName) && init2 !== null && init2 !== void 0) {
8731
- constants.push({ name: varName, line: node.loc?.start.line ?? 1 });
8732
- continue;
8733
- }
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
- });
8555
+ const sourcesDigest = computeSourcesDigest(files, root);
8556
+ const yamlBody = renderYaml(result.entries, {
8557
+ generator: `cdd-kit ${_pkg.version}`,
8558
+ sourcesDigest
8559
+ });
8560
+ const totalSrc = result.entries.reduce((s, e) => s + e.total_lines, 0);
8561
+ const mapLines = yamlBody.split("\n").length;
8562
+ const compression = totalSrc === 0 ? 0 : totalSrc / mapLines;
8563
+ const summaryLine = `scanned ${result.entries.length} files, ${totalSrc} src lines -> ${opts.out} (${mapLines} lines, compression ${compression.toFixed(1)}x)`;
8564
+ for (const w of result.warnings) {
8565
+ log.warn(`${w.path}: ${w.message}`);
8566
+ }
8567
+ if (opts.check) {
8568
+ const existing = existsSync11(opts.out) ? readFileSync14(opts.out, "utf8") : "";
8569
+ const normalize = (s) => s.replace(/^# generated: [^\n]+\n/m, "# generated: <normalized>\n");
8570
+ if (normalize(existing) !== normalize(yamlBody)) {
8571
+ log.error(`code-map out of date: ${opts.out} would change. Run \`cdd-kit code-map\` to regenerate.`);
8572
+ return 1;
8748
8573
  }
8574
+ log.ok(`code-map up to date: ${opts.out}`);
8575
+ return 0;
8749
8576
  }
8577
+ mkdirSync5(dirname4(opts.out), { recursive: true });
8578
+ writeFileSync6(opts.out, yamlBody, "utf8");
8579
+ log.ok(`${summaryLine} (${Date.now() - start}ms)`);
8580
+ return 0;
8750
8581
  }
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);
8582
+ var _require, _pkgPath, _pkg;
8583
+ var init_code_map = __esm({
8584
+ "src/commands/code-map.ts"() {
8585
+ "use strict";
8586
+ init_logger();
8587
+ init_yaml_writer();
8588
+ init_orchestrator();
8589
+ init_config();
8590
+ init_digest();
8591
+ _require = createRequire(import.meta.url);
8592
+ _pkgPath = join14(fileURLToPath2(import.meta.url), "..", "..", "..", "package.json");
8593
+ _pkg = JSON.parse(readFileSync14(_pkgPath, "utf8"));
8786
8594
  }
8787
- return {
8788
- name: node.id.name,
8789
- lines: getLineRange(node),
8790
- exported,
8791
- members
8792
- };
8793
- }
8794
- function processStatement(stmt, buckets, extractTsTypes, exportedFromWrapper = false) {
8795
- if (stmt.type === "ExportNamedDeclaration" && stmt.declaration) {
8796
- processStatement(stmt.declaration, buckets, extractTsTypes, true);
8797
- return;
8595
+ });
8596
+
8597
+ // src/code-map/freshness.ts
8598
+ var freshness_exports = {};
8599
+ __export(freshness_exports, {
8600
+ checkCodeMapFreshness: () => checkCodeMapFreshness
8601
+ });
8602
+ import { existsSync as existsSync12, readFileSync as readFileSync15, statSync as statSync3 } from "fs";
8603
+ import { join as join15 } from "path";
8604
+ function checkCodeMapFreshness(cwd, mapRel = ".cdd/code-map.yml", include, exclude) {
8605
+ const mapPath = join15(cwd, mapRel);
8606
+ let cfg;
8607
+ try {
8608
+ cfg = loadCodeMapConfig(cwd);
8609
+ } catch (err) {
8610
+ return {
8611
+ status: "config-error",
8612
+ staleFiles: [],
8613
+ staleCount: 0,
8614
+ mapPath,
8615
+ configError: err.message
8616
+ };
8798
8617
  }
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);
8618
+ const includeFinal = [...cfg.include, ...include ?? []];
8619
+ const excludeFinal = [...cfg.exclude, ...exclude ?? []];
8620
+ const sourceFiles = walkRepo(cwd, { include: includeFinal, exclude: excludeFinal });
8621
+ if (!existsSync12(mapPath)) {
8622
+ if (sourceFiles.length === 0) {
8623
+ return { status: "missing-greenfield", staleFiles: [], staleCount: 0, mapPath };
8809
8624
  }
8810
- return;
8811
- }
8812
- if (stmt.type === "ImportDeclaration") {
8813
- buckets.imports.push(processImportDeclaration(stmt));
8814
- return;
8815
- }
8816
- if (stmt.type === "FunctionDeclaration") {
8817
- const fe = processFunctionDeclaration(stmt);
8818
- if (fe)
8819
- buckets.functions.push(fe);
8820
- return;
8821
- }
8822
- if (stmt.type === "ClassDeclaration") {
8823
- const ce = processClassDeclaration(stmt);
8824
- if (ce)
8825
- buckets.classes.push(ce);
8826
- return;
8827
- }
8828
- if (stmt.type === "VariableDeclaration") {
8829
- processVariableDeclaration(stmt, buckets.imports, buckets.constants, buckets.functions);
8830
- return;
8625
+ return { status: "missing-with-sources", staleFiles: [], staleCount: 0, mapPath };
8831
8626
  }
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;
8627
+ const mapMtime = statSync3(mapPath).mtimeMs;
8628
+ const staleAll = [];
8629
+ for (const absPath of sourceFiles) {
8630
+ try {
8631
+ const mtime = statSync3(absPath).mtimeMs;
8632
+ if (mtime > mapMtime) {
8633
+ const rel = absPath.replace(/\\/g, "/").replace(cwd.replace(/\\/g, "/") + "/", "");
8634
+ staleAll.push(rel);
8635
+ }
8636
+ } catch {
8851
8637
  }
8852
8638
  }
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
- }
8639
+ if (staleAll.length === 0) {
8640
+ return { status: "ok", staleFiles: [], staleCount: 0, mapPath };
8641
+ }
8642
+ const declaredDigest = readSourcesDigest(mapPath);
8643
+ if (declaredDigest !== null) {
8644
+ const actualDigest = computeSourcesDigest(sourceFiles, cwd);
8645
+ if (actualDigest === declaredDigest) {
8646
+ return { status: "ok", staleFiles: [], staleCount: 0, mapPath };
8860
8647
  }
8861
8648
  }
8649
+ return {
8650
+ status: "stale",
8651
+ staleFiles: staleAll.slice(0, 5),
8652
+ staleCount: staleAll.length,
8653
+ mapPath
8654
+ };
8862
8655
  }
8863
- function parseAndExtract(source, opts) {
8864
- let ast;
8656
+ function readSourcesDigest(mapPath) {
8865
8657
  try {
8866
- ast = parseSourceWithPlugins(source, opts.plugins);
8658
+ const head = readFileSync15(mapPath, "utf8").slice(0, 2048);
8659
+ const m = head.match(/^# sources-digest:\s*([a-f0-9]+)/m);
8660
+ return m ? m[1] : null;
8867
8661
  } catch {
8868
8662
  return null;
8869
8663
  }
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);
8881
- }
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;
8894
- return {
8895
- path: relPath,
8896
- total_lines,
8897
- imports: r.imports,
8898
- constants: r.constants,
8899
- classes: r.classes,
8900
- functions: r.functions
8901
- };
8902
8664
  }
8903
- var COMMON_PLUGINS, JS_PLUGINS, JavaScriptScanner, jsScanner;
8904
- var init_javascript = __esm({
8905
- "src/code-map/scanners/javascript.ts"() {
8665
+ var init_freshness = __esm({
8666
+ "src/code-map/freshness.ts"() {
8906
8667
  "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);
8937
- }
8938
- };
8939
- jsScanner = new JavaScriptScanner();
8668
+ init_include_exclude();
8669
+ init_config();
8670
+ init_code_map();
8940
8671
  }
8941
8672
  });
8942
8673
 
8943
- // src/code-map/scanners/typescript.ts
8944
- var typescript_exports = {};
8945
- __export(typescript_exports, {
8946
- tsScanner: () => tsScanner
8674
+ // src/commands/migrate.ts
8675
+ var migrate_exports = {};
8676
+ __export(migrate_exports, {
8677
+ migrate: () => migrate
8947
8678
  });
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
- };
8985
- }
8986
- };
8987
- tsScanner = new TypeScriptScanner();
8679
+ import { join as join18 } from "path";
8680
+ import { cpSync as cpSync2, existsSync as existsSync15, mkdirSync as mkdirSync7, readdirSync as readdirSync8, readFileSync as readFileSync18, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync8 } from "fs";
8681
+ import yaml3 from "js-yaml";
8682
+ function backupChangeDir(cwd, changeId, sessionStamp) {
8683
+ const backupRoot = join18(cwd, ".cdd", "migrate-backup", sessionStamp);
8684
+ const backupDir2 = join18(backupRoot, changeId);
8685
+ mkdirSync7(backupRoot, { recursive: true });
8686
+ const sourceDir = join18(cwd, "specs", "changes", changeId);
8687
+ if (existsSync15(sourceDir)) {
8688
+ cpSync2(sourceDir, backupDir2, { recursive: true });
8988
8689
  }
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;
9008
- try {
9009
- source = readFileSync16(absolutePath, "utf8");
9010
- } catch (err) {
9011
- throw err;
9012
- }
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
- };
8690
+ return backupDir2;
8691
+ }
8692
+ function buildLegacyContextManifest(changeId) {
8693
+ return [
8694
+ "# Context Manifest",
8695
+ "",
8696
+ "Generated by `cdd-kit migrate` for an existing change.",
8697
+ "Legacy manifest. Forbidden paths come from `.cdd/context-policy.json`.",
8698
+ "",
8699
+ "## Affected Surfaces",
8700
+ "- legacy-unknown",
8701
+ "",
8702
+ "## Allowed Paths",
8703
+ `- specs/changes/${changeId}/`,
8704
+ "",
8705
+ "## Required Contracts",
8706
+ "- legacy-unknown",
8707
+ "",
8708
+ "## Required Tests",
8709
+ "- legacy-unknown",
8710
+ "",
8711
+ "## Agent Work Packets",
8712
+ "",
8713
+ "## Context Expansion Requests",
8714
+ "-",
8715
+ "",
8716
+ "## Approved Expansions",
8717
+ "-",
8718
+ ""
8719
+ ].join("\n");
8720
+ }
8721
+ function buildContextGovernedManifest(changeId) {
8722
+ return [
8723
+ "# Context Manifest",
8724
+ "",
8725
+ "Generated by `cdd-kit migrate --enable-context-governance` for an existing change.",
8726
+ "Review and narrow the allowed paths before assigning implementation work.",
8727
+ "Forbidden paths come from `.cdd/context-policy.json`.",
8728
+ "",
8729
+ "## Affected Surfaces",
8730
+ "- legacy-unknown",
8731
+ "",
8732
+ "## Allowed Paths",
8733
+ `- specs/changes/${changeId}/`,
8734
+ "- specs/context/project-map.md",
8735
+ "- specs/context/contracts-index.md",
8736
+ "",
8737
+ "## Required Contracts",
8738
+ "- legacy-unknown",
8739
+ "",
8740
+ "## Required Tests",
8741
+ "- legacy-unknown",
8742
+ "",
8743
+ "## Agent Work Packets",
8744
+ "",
8745
+ "### change-classifier",
8746
+ "- allowed:",
8747
+ ` - specs/changes/${changeId}/`,
8748
+ " - specs/context/project-map.md",
8749
+ " - specs/context/contracts-index.md",
8750
+ "",
8751
+ "## Context Expansion Requests",
8752
+ "",
8753
+ "<!--",
8754
+ "Agents must request context expansion instead of reading outside their work packet.",
8755
+ "Use this format only for real requests:",
8756
+ "",
8757
+ "- request-id: CER-001",
8758
+ " requested_paths:",
8759
+ " - src/example.ts",
8760
+ " reason: why this file is required",
8761
+ " status: pending",
8762
+ "-->",
8763
+ "",
8764
+ "## Approved Expansions",
8765
+ "-",
8766
+ ""
8767
+ ].join("\n");
8768
+ }
8769
+ function parseLegacyFrontmatter(content) {
8770
+ const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
8771
+ if (!m)
8772
+ return {};
8773
+ const out = {};
8774
+ for (const line of m[1].split(/\r?\n/)) {
8775
+ const colon = line.indexOf(":");
8776
+ if (colon === -1)
8777
+ continue;
8778
+ const key = line.slice(0, colon).trim();
8779
+ if (!key)
8780
+ continue;
8781
+ out[key] = line.slice(colon + 1).trim();
8782
+ }
8783
+ return out;
8784
+ }
8785
+ function parseListField(raw) {
8786
+ if (!raw)
8787
+ return [];
8788
+ const trimmed = raw.trim();
8789
+ if (!trimmed || trimmed === "[]")
8790
+ return [];
8791
+ const inner = trimmed.startsWith("[") && trimmed.endsWith("]") ? trimmed.slice(1, -1) : trimmed;
8792
+ return inner.split(",").map((item) => item.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
8793
+ }
8794
+ function parseLegacyTaskList(body) {
8795
+ const lines = body.split(/\r?\n/);
8796
+ const rows = [];
8797
+ let currentSection;
8798
+ for (const raw of lines) {
8799
+ const headerMatch = raw.match(/^##\s+\d+\.\s+(.*)\s*$/);
8800
+ if (headerMatch) {
8801
+ currentSection = headerMatch[1].trim();
8802
+ continue;
8803
+ }
8804
+ const itemMatch = raw.match(/^\s*-\s*\[([ xX\-])\]\s+(\d+(?:\.\d+)*)\s+(.*)\s*$/);
8805
+ if (!itemMatch)
8806
+ continue;
8807
+ const mark = itemMatch[1];
8808
+ const id = itemMatch[2];
8809
+ const title = itemMatch[3].trim();
8810
+ let status = "pending";
8811
+ if (mark === "x" || mark === "X")
8812
+ status = "done";
8813
+ else if (mark === "-")
8814
+ status = "skipped";
8815
+ rows.push({ id, title, status, section: currentSection });
8816
+ }
8817
+ return rows;
8818
+ }
8819
+ function migrateTasksFile(changeId, changeDir, enableContextGovernance, detectedTier, changed, warnings, pendingWrites, pendingDeletes) {
8820
+ const newPath = join18(changeDir, "tasks.yml");
8821
+ const legacyPath = join18(changeDir, "tasks.md");
8822
+ if (existsSync15(newPath)) {
8823
+ return;
8824
+ }
8825
+ if (!existsSync15(legacyPath)) {
8826
+ warnings.push("tasks.md not found and tasks.yml missing \u2014 skipping tasks migration");
8827
+ return;
8828
+ }
8829
+ const raw = readFileSync18(legacyPath, "utf8");
8830
+ const fm = parseLegacyFrontmatter(raw);
8831
+ const bodyMatch = raw.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?([\s\S]*)$/);
8832
+ const body = bodyMatch ? bodyMatch[1] : raw;
8833
+ const tasksRows = parseLegacyTaskList(body);
8834
+ const data = {};
8835
+ data["change-id"] = fm["change-id"] || changeId;
8836
+ data["status"] = fm["status"] || "in-progress";
8837
+ if (fm["tier"] && /^\d+$/.test(fm["tier"])) {
8838
+ data["tier"] = parseInt(fm["tier"], 10);
8839
+ } else if (detectedTier) {
8840
+ data["tier"] = parseInt(detectedTier, 10);
8841
+ } else {
8842
+ data["tier"] = null;
8843
+ }
8844
+ if (enableContextGovernance || fm["context-governance"] === "v1") {
8845
+ data["context-governance"] = "v1";
8846
+ }
8847
+ const archive2 = parseListField(fm["archive-tasks"]);
8848
+ data["archive-tasks"] = archive2.length > 0 ? archive2 : ["7.1", "7.2"];
8849
+ const deps = parseListField(fm["depends-on"]);
8850
+ data["depends-on"] = deps;
8851
+ data["tasks"] = tasksRows.map((r) => {
8852
+ const out = { id: r.id, title: r.title, status: r.status };
8853
+ if (r.section)
8854
+ out["section"] = r.section;
8855
+ return out;
8856
+ });
8857
+ const yamlOut = yaml3.dump(data, { lineWidth: -1, noRefs: true });
8858
+ pendingWrites.push({ path: newPath, content: yamlOut });
8859
+ pendingDeletes.push({ path: legacyPath });
8860
+ changed.push(`tasks.md -> tasks.yml (${tasksRows.length} task(s) migrated)`);
8861
+ }
8862
+ function parseLegacyAgentLog(content) {
8863
+ const lines = content.split(/\r?\n/);
8864
+ const data = {};
8865
+ let i = 0;
8866
+ const topFieldRe = /^[ ]{0,1}-\s*([\w-]+):\s*(.*)$/;
8867
+ while (i < lines.length) {
8868
+ const line = lines[i];
8869
+ const fieldMatch = line.match(topFieldRe);
8870
+ if (!fieldMatch) {
8871
+ i++;
8872
+ continue;
8873
+ }
8874
+ const key = fieldMatch[1];
8875
+ const inline = fieldMatch[2].trim();
8876
+ if (key === "files-read" || key === "artifacts") {
8877
+ const items = [];
8878
+ let j = i + 1;
8879
+ while (j < lines.length) {
8880
+ const sub = lines[j];
8881
+ if (topFieldRe.test(sub))
8882
+ break;
8883
+ if (/^#/.test(sub))
8884
+ break;
8885
+ const itemMatch = sub.match(/^\s{2,}-\s+(.+?)\s*$/);
8886
+ if (itemMatch) {
8887
+ items.push(itemMatch[1].trim());
9034
8888
  }
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
- });
8889
+ j++;
8890
+ }
8891
+ if (key === "files-read") {
8892
+ data["files-read"] = items;
8893
+ } else {
8894
+ data["artifacts"] = items.map((s) => {
8895
+ const idx = s.indexOf(":");
8896
+ if (idx === -1) {
8897
+ return { type: "note", pointer: s };
9074
8898
  }
8899
+ const type = s.slice(0, idx).trim();
8900
+ const pointer = s.slice(idx + 1).trim();
8901
+ return { type, pointer };
8902
+ });
8903
+ }
8904
+ i = j;
8905
+ continue;
8906
+ }
8907
+ data[key] = inline;
8908
+ i++;
8909
+ }
8910
+ return data;
8911
+ }
8912
+ function migrateAgentLogs(changeDir, changed, pendingWrites, pendingDeletes) {
8913
+ const agentLogDir = join18(changeDir, "agent-log");
8914
+ if (!existsSync15(agentLogDir))
8915
+ return;
8916
+ const mdLogs = readdirSync8(agentLogDir).filter((f) => f.endsWith(".md"));
8917
+ for (const f of mdLogs) {
8918
+ const fullPath = join18(agentLogDir, f);
8919
+ const yamlName = f.replace(/\.md$/, ".yml");
8920
+ const yamlFull = join18(agentLogDir, yamlName);
8921
+ if (existsSync15(yamlFull))
8922
+ continue;
8923
+ const raw = readFileSync18(fullPath, "utf8");
8924
+ const parsed = parseLegacyAgentLog(raw);
8925
+ const yamlOut = yaml3.dump(parsed, { lineWidth: -1, noRefs: true });
8926
+ pendingWrites.push({ path: yamlFull, content: yamlOut });
8927
+ pendingDeletes.push({ path: fullPath });
8928
+ changed.push(`agent-log/${f} -> agent-log/${yamlName}`);
8929
+ }
8930
+ }
8931
+ function migrateOne(changeId, changeDir, enableContextGovernance) {
8932
+ const changed = [];
8933
+ const warnings = [];
8934
+ const pending = [];
8935
+ const deletes = [];
8936
+ let detectedTier = null;
8937
+ const classifPath = join18(changeDir, "change-classification.md");
8938
+ if (existsSync15(classifPath)) {
8939
+ const content = readFileSync18(classifPath, "utf8");
8940
+ const hasNewTierFormat = /^## Tier\s*\n\s*-\s*\d\s*$/m.test(content);
8941
+ const oldMatch = content.match(/\*\*Tier[:\*]+\s*(?:Tier\s*)?(\d)/i) ?? content.match(/^-?\s*Tier:\s*(?:Tier\s*)?(\d)/mi);
8942
+ if (oldMatch)
8943
+ detectedTier = oldMatch[1];
8944
+ if (!hasNewTierFormat) {
8945
+ if (detectedTier) {
8946
+ const addition = `
8947
+ ## Tier
8948
+ - ${detectedTier}
8949
+ `;
8950
+ if (!content.includes("\n## Tier\n")) {
8951
+ changed.push(
8952
+ `change-classification.md: appended "## Tier\\n- ${detectedTier}" (converted from old format)`
8953
+ );
8954
+ pending.push({ path: classifPath, content: content + addition });
8955
+ }
8956
+ } else {
8957
+ warnings.push(
8958
+ "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."
8959
+ );
8960
+ }
8961
+ } else {
8962
+ const structured = content.match(/^## Tier\s*\n\s*-\s*(\d)\s*$/m);
8963
+ if (structured)
8964
+ detectedTier = structured[1];
8965
+ }
8966
+ }
8967
+ migrateTasksFile(changeId, changeDir, enableContextGovernance, detectedTier, changed, warnings, pending, deletes);
8968
+ migrateAgentLogs(changeDir, changed, pending, deletes);
8969
+ const manifestPath = join18(changeDir, "context-manifest.md");
8970
+ if (!existsSync15(manifestPath)) {
8971
+ changed.push(enableContextGovernance ? "context-manifest.md: added context-governance v1 manifest scaffold" : "context-manifest.md: added legacy context manifest scaffold");
8972
+ pending.push({
8973
+ path: manifestPath,
8974
+ content: enableContextGovernance ? buildContextGovernedManifest(changeId) : buildLegacyContextManifest(changeId)
8975
+ });
8976
+ } else if (enableContextGovernance) {
8977
+ warnings.push("context-manifest.md already exists \u2014 review allowed paths before relying on context-governance: v1");
8978
+ }
8979
+ return { result: { changed, warnings }, pending, deletes };
8980
+ }
8981
+ function commitWritesAtomically(pending, deletes) {
8982
+ const renames = [];
8983
+ try {
8984
+ for (const write of pending) {
8985
+ const tmp = `${write.path}.cdd-migrate.tmp`;
8986
+ writeFileSync8(tmp, write.content, "utf8");
8987
+ renames.push({ tmp, final: write.path });
8988
+ }
8989
+ } catch (err) {
8990
+ for (const r of renames) {
8991
+ try {
8992
+ rmSync2(r.tmp, { force: true });
8993
+ } catch {
8994
+ }
8995
+ }
8996
+ throw err;
8997
+ }
8998
+ for (const r of renames) {
8999
+ renameSync(r.tmp, r.final);
9000
+ }
9001
+ for (const d of deletes) {
9002
+ try {
9003
+ rmSync2(d.path, { force: true });
9004
+ } catch {
9005
+ }
9006
+ }
9007
+ }
9008
+ async function migrate(changeId, opts = {}) {
9009
+ const cwd = process.cwd();
9010
+ const dryRun = opts.dryRun ?? false;
9011
+ const enableContextGovernance = opts.enableContextGovernance ?? false;
9012
+ const noBackup = opts.noBackup ?? false;
9013
+ const idsToMigrate = [];
9014
+ if (opts.all) {
9015
+ const changesDir = join18(cwd, "specs", "changes");
9016
+ if (!existsSync15(changesDir)) {
9017
+ log.info("No specs/changes/ directory found \u2014 nothing to migrate.");
9018
+ return;
9019
+ }
9020
+ idsToMigrate.push(
9021
+ ...readdirSync8(changesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name)
9022
+ );
9023
+ } else if (changeId) {
9024
+ const specificDir = join18(cwd, "specs", "changes", changeId);
9025
+ if (!existsSync15(specificDir)) {
9026
+ log.error(`Change not found: specs/changes/${changeId}`);
9027
+ process.exit(1);
9028
+ }
9029
+ idsToMigrate.push(changeId);
9030
+ } else {
9031
+ log.error("Usage: cdd-kit migrate <change-id> | cdd-kit migrate --all [--dry-run] [--no-backup]");
9032
+ process.exit(1);
9033
+ }
9034
+ if (idsToMigrate.length === 0) {
9035
+ log.info("No changes found to migrate.");
9036
+ return;
9037
+ }
9038
+ if (dryRun) {
9039
+ log.info("Dry run \u2014 no files will be written.");
9040
+ log.blank();
9041
+ }
9042
+ const sessionStamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
9043
+ let migratedCount = 0;
9044
+ let upToDateCount = 0;
9045
+ const backupRoot = join18(cwd, ".cdd", "migrate-backup", sessionStamp);
9046
+ for (const id of idsToMigrate) {
9047
+ const changeDir = join18(cwd, "specs", "changes", id);
9048
+ if (!existsSync15(changeDir)) {
9049
+ log.warn(` ${id}: directory not found \u2014 skipping`);
9050
+ continue;
9051
+ }
9052
+ const { result, pending, deletes } = migrateOne(id, changeDir, enableContextGovernance);
9053
+ const { changed, warnings } = result;
9054
+ if (changed.length === 0) {
9055
+ log.info(` ${id}: already up to date`);
9056
+ upToDateCount++;
9057
+ for (const w of warnings)
9058
+ log.warn(` ${id}: ${w}`);
9059
+ continue;
9060
+ }
9061
+ if (!dryRun) {
9062
+ try {
9063
+ if (!noBackup)
9064
+ backupChangeDir(cwd, id, sessionStamp);
9065
+ commitWritesAtomically(pending, deletes);
9066
+ } catch (err) {
9067
+ log.error(` ${id}: migration failed \u2014 ${err.message}`);
9068
+ if (!noBackup) {
9069
+ log.error(` ${id}: restore from .cdd/migrate-backup/${sessionStamp}/${id}/`);
9075
9070
  }
9076
- return {
9077
- path: relPath,
9078
- total_lines,
9079
- imports: allImports,
9080
- constants: allConstants,
9081
- classes: allClasses,
9082
- functions: allFunctions
9083
- };
9071
+ process.exit(1);
9084
9072
  }
9085
- };
9086
- vueScanner = new VueScanner();
9073
+ }
9074
+ log.ok(` ${id}: migrated`);
9075
+ for (const c of changed)
9076
+ log.info(` + ${c}`);
9077
+ migratedCount++;
9078
+ for (const w of warnings)
9079
+ log.warn(` ${id}: ${w}`);
9080
+ }
9081
+ log.blank();
9082
+ if (dryRun) {
9083
+ log.info(`Dry run complete: ${migratedCount} change(s) would be updated, ${upToDateCount} already up to date.`);
9084
+ } else {
9085
+ log.ok(`Migration complete: ${migratedCount} updated, ${upToDateCount} already up to date.`);
9086
+ if (migratedCount > 0 && !noBackup) {
9087
+ log.info(`Backup: ${backupRoot}`);
9088
+ if (ensureGitignoreEntry(cwd, ".cdd/migrate-backup/")) {
9089
+ log.info("Added `.cdd/migrate-backup/` to .gitignore");
9090
+ }
9091
+ log.info('Next: git add specs/changes/ && git commit -m "chore: migrate changes to YAML format"');
9092
+ log.info("When stable, remove backup: rm -rf .cdd/migrate-backup/");
9093
+ }
9094
+ }
9095
+ }
9096
+ function ensureGitignoreEntry(cwd, entry) {
9097
+ const path = join18(cwd, ".gitignore");
9098
+ const trimmed = entry.trim();
9099
+ if (!trimmed)
9100
+ return false;
9101
+ const re = new RegExp(`^\\s*${trimmed.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\$&")}\\s*$`, "m");
9102
+ let existing = "";
9103
+ if (existsSync15(path))
9104
+ existing = readFileSync18(path, "utf8");
9105
+ if (re.test(existing))
9106
+ return false;
9107
+ const sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
9108
+ const block = existing.length === 0 ? `# cdd-kit generated backups (do not commit)
9109
+ ${trimmed}
9110
+ ` : `${sep}
9111
+ # cdd-kit generated backups (do not commit)
9112
+ ${trimmed}
9113
+ `;
9114
+ writeFileSync8(path, existing + block, "utf8");
9115
+ return true;
9116
+ }
9117
+ var init_migrate = __esm({
9118
+ "src/commands/migrate.ts"() {
9119
+ "use strict";
9120
+ init_logger();
9087
9121
  }
9088
9122
  });
9089
9123
 
9090
- // src/commands/code-map.ts
9091
- var code_map_exports = {};
9092
- __export(code_map_exports, {
9093
- codeMap: () => codeMap
9124
+ // src/commands/upgrade.ts
9125
+ var upgrade_exports = {};
9126
+ __export(upgrade_exports, {
9127
+ upgrade: () => upgrade
9094
9128
  });
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));
9129
+ import { existsSync as existsSync16, mkdirSync as mkdirSync8, readdirSync as readdirSync9, copyFileSync as copyFileSync3, readFileSync as readFileSync19, writeFileSync as writeFileSync9 } from "fs";
9130
+ import { dirname as dirname5, join as join19, relative as relative6 } from "path";
9131
+ function planMissingFiles(srcDir, destDir, label, planned) {
9132
+ if (!existsSync16(srcDir))
9133
+ return;
9134
+ for (const entry of readdirSync9(srcDir, { withFileTypes: true })) {
9135
+ const src = join19(srcDir, entry.name);
9136
+ const dest = join19(destDir, entry.name);
9137
+ if (entry.isDirectory()) {
9138
+ planMissingFiles(src, dest, join19(label, entry.name), planned);
9139
+ continue;
9140
+ }
9141
+ if (!existsSync16(dest)) {
9142
+ planned.push({ src, dest, rel: join19(label, relative6(srcDir, src)) });
9120
9143
  }
9121
9144
  }
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));
9145
+ }
9146
+ function planProviderGuidance(cwd, provider, planned) {
9147
+ if (provider === "claude" || provider === "both") {
9148
+ if (!existsSync16(join19(cwd, "CLAUDE.md"))) {
9149
+ planned.push({ src: ASSET.claudeTemplate, dest: join19(cwd, "CLAUDE.md"), rel: "CLAUDE.md" });
9150
+ }
9151
+ if (!existsSync16(join19(cwd, "AGENTS.md"))) {
9152
+ planned.push({ src: ASSET.agentsTemplate, dest: join19(cwd, "AGENTS.md"), rel: "AGENTS.md" });
9153
+ }
9131
9154
  }
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));
9155
+ if ((provider === "codex" || provider === "both") && !existsSync16(join19(cwd, "CODEX.md"))) {
9156
+ planned.push({ src: ASSET.codexTemplate, dest: join19(cwd, "CODEX.md"), rel: "CODEX.md" });
9139
9157
  }
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));
9158
+ }
9159
+ function applyCopy(plan) {
9160
+ for (const item of plan) {
9161
+ mkdirSync8(dirname5(item.dest), { recursive: true });
9162
+ copyFileSync3(item.src, item.dest);
9143
9163
  }
9144
- for (const r of await Promise.all(tasks)) {
9145
- result.entries.push(...r.entries);
9146
- result.warnings.push(...r.warnings);
9164
+ }
9165
+ async function upgrade(opts = {}) {
9166
+ const cwd = process.cwd();
9167
+ const requestedProvider = opts.provider ?? "auto";
9168
+ if (!validateProviderOption(requestedProvider)) {
9169
+ log.error(`Invalid provider: ${requestedProvider}. Use auto, claude, codex, or both.`);
9170
+ process.exit(1);
9147
9171
  }
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})`
9172
+ const provider = inferProvider(cwd, requestedProvider);
9173
+ const plan = [];
9174
+ planMissingFiles(ASSET.contracts, join19(cwd, "contracts"), "contracts", plan);
9175
+ planMissingFiles(ASSET.specsTemplates, join19(cwd, "specs", "templates"), "specs/templates", plan);
9176
+ planMissingFiles(ASSET.testsTemplates, join19(cwd, "tests", "templates"), "tests/templates", plan);
9177
+ planMissingFiles(ASSET.ci, join19(cwd, "ci"), "ci", plan);
9178
+ planMissingFiles(ASSET.githubWorkflows, join19(cwd, ".github", "workflows"), ".github/workflows", plan);
9179
+ planMissingFiles(ASSET.cddConfig, join19(cwd, ".cdd"), ".cdd", plan);
9180
+ planProviderGuidance(cwd, provider, plan);
9181
+ log.blank();
9182
+ log.info(`Upgrade provider: ${provider}`);
9183
+ if (plan.length === 0) {
9184
+ log.ok("No missing cdd-kit project files found.");
9185
+ if (opts.migrateChanges) {
9186
+ log.blank();
9187
+ log.info("Running change migration flow...");
9188
+ await migrate(void 0, {
9189
+ all: true,
9190
+ dryRun: !opts.yes,
9191
+ enableContextGovernance: opts.enableContextGovernance
9154
9192
  });
9155
9193
  }
9194
+ log.blank();
9195
+ return;
9156
9196
  }
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}`);
9197
+ log.info(`${plan.length} missing file(s) detected:`);
9198
+ for (const item of plan)
9199
+ log.dim(` + ${item.rel.replace(/\\/g, "/")}`);
9200
+ if (!opts.yes) {
9201
+ log.blank();
9202
+ log.info("Dry run only. Re-run with --yes to write missing files.");
9203
+ if (opts.migrateChanges) {
9204
+ log.blank();
9205
+ log.info("Previewing existing change migration because --migrate-changes was requested.");
9206
+ await migrate(void 0, {
9207
+ all: true,
9208
+ dryRun: true,
9209
+ enableContextGovernance: opts.enableContextGovernance
9210
+ });
9211
+ }
9212
+ log.blank();
9213
+ return;
9164
9214
  }
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;
9215
+ applyCopy(plan);
9216
+ const modelPolicyPath = join19(cwd, ".cdd", "model-policy.json");
9217
+ if (existsSync16(modelPolicyPath)) {
9218
+ let existing = {};
9219
+ try {
9220
+ existing = JSON.parse(readFileSync19(modelPolicyPath, "utf8"));
9221
+ } catch {
9171
9222
  }
9172
- log.ok(`code-map up to date: ${opts.out}`);
9173
- return 0;
9223
+ const merged = {
9224
+ ...existing,
9225
+ provider,
9226
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
9227
+ roles: existing.roles && typeof existing.roles === "object" ? existing.roles : {}
9228
+ };
9229
+ writeFileSync9(modelPolicyPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
9174
9230
  }
9175
- mkdirSync8(dirname5(opts.out), { recursive: true });
9176
- writeFileSync9(opts.out, yamlBody, "utf8");
9177
- log.ok(`${summaryLine} (${Date.now() - start}ms)`);
9178
- return 0;
9231
+ log.blank();
9232
+ log.ok(`Upgrade complete: ${plan.length} missing file(s) added.`);
9233
+ log.info("Existing project guidance and contracts were preserved.");
9234
+ if (opts.migrateChanges) {
9235
+ log.blank();
9236
+ log.info("Running change migration flow...");
9237
+ await migrate(void 0, {
9238
+ all: true,
9239
+ dryRun: false,
9240
+ enableContextGovernance: opts.enableContextGovernance
9241
+ });
9242
+ }
9243
+ log.blank();
9179
9244
  }
9180
- var _require, _pkgPath, _pkg;
9181
- var init_code_map = __esm({
9182
- "src/commands/code-map.ts"() {
9245
+ var init_upgrade = __esm({
9246
+ "src/commands/upgrade.ts"() {
9183
9247
  "use strict";
9248
+ init_paths();
9184
9249
  init_logger();
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"));
9250
+ init_provider();
9251
+ init_migrate();
9191
9252
  }
9192
9253
  });
9193
9254
 
@@ -9196,11 +9257,11 @@ var refresh_exports = {};
9196
9257
  __export(refresh_exports, {
9197
9258
  refresh: () => refresh
9198
9259
  });
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";
9260
+ import { existsSync as existsSync17, mkdirSync as mkdirSync9, readdirSync as readdirSync10, copyFileSync as copyFileSync4, readFileSync as readFileSync20, writeFileSync as writeFileSync10 } from "fs";
9261
+ import { dirname as dirname6, join as join20, relative as relative7 } from "path";
9262
+ import { createHash as createHash6 } from "crypto";
9202
9263
  function fileHash2(filePath) {
9203
- return createHash4("sha256").update(readFileSync18(filePath)).digest("hex");
9264
+ return createHash6("sha256").update(readFileSync20(filePath)).digest("hex");
9204
9265
  }
9205
9266
  function planForceRefresh(srcDir, destDir, sectionLabel) {
9206
9267
  const plan = [];
@@ -9222,7 +9283,7 @@ function planForceRefresh(srcDir, destDir, sectionLabel) {
9222
9283
  }
9223
9284
  if (!item.isFile())
9224
9285
  continue;
9225
- const rel = join20(sectionLabel, relative5(srcDir, sp)).replace(/\\/g, "/");
9286
+ const rel = join20(sectionLabel, relative7(srcDir, sp)).replace(/\\/g, "/");
9226
9287
  if (!existsSync17(dp)) {
9227
9288
  plan.push({ src: sp, dest: dp, rel, action: "add" });
9228
9289
  } else if (fileHash2(sp) !== fileHash2(dp)) {
@@ -9252,7 +9313,7 @@ function ensureGitignoreEntry2(cwd, entry) {
9252
9313
  const re = new RegExp(`^\\s*${trimmed.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\$&")}\\s*$`, "m");
9253
9314
  let existing = "";
9254
9315
  if (existsSync17(path))
9255
- existing = readFileSync18(path, "utf8");
9316
+ existing = readFileSync20(path, "utf8");
9256
9317
  if (re.test(existing))
9257
9318
  return false;
9258
9319
  const sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
@@ -9308,7 +9369,7 @@ function resyncModelPolicy(cwd) {
9308
9369
  const desired = {};
9309
9370
  const agentFiles = readdirSync10(AGENTS_HOME, { withFileTypes: true }).filter((d) => d.isFile() && d.name.endsWith(".md"));
9310
9371
  for (const f of agentFiles) {
9311
- const content = readFileSync18(join20(AGENTS_HOME, f.name), "utf8");
9372
+ const content = readFileSync20(join20(AGENTS_HOME, f.name), "utf8");
9312
9373
  const fm = parseAgentFrontmatter(content);
9313
9374
  if (fm.name && fm.model)
9314
9375
  desired[fm.name] = fm.model;
@@ -9318,7 +9379,7 @@ function resyncModelPolicy(cwd) {
9318
9379
  let existing = {};
9319
9380
  if (existsSync17(policyPath)) {
9320
9381
  try {
9321
- existing = JSON.parse(readFileSync18(policyPath, "utf8"));
9382
+ existing = JSON.parse(readFileSync20(policyPath, "utf8"));
9322
9383
  } catch {
9323
9384
  }
9324
9385
  }
@@ -9422,7 +9483,7 @@ async function refresh(opts) {
9422
9483
  templateOverwritten = result.overwritten;
9423
9484
  log.ok(` applied: +${templateAdded} added, ~${templateOverwritten} overwritten`);
9424
9485
  if (templateOverwritten > 0) {
9425
- log.info(` backup saved to: ${relative5(cwd, backupRoot).replace(/\\/g, "/")}`);
9486
+ log.info(` backup saved to: ${relative7(cwd, backupRoot).replace(/\\/g, "/")}`);
9426
9487
  if (ensureGitignoreEntry2(cwd, ".cdd/.refresh-backup/")) {
9427
9488
  log.info(" added `.cdd/.refresh-backup/` to .gitignore");
9428
9489
  }
@@ -9498,7 +9559,7 @@ async function refresh(opts) {
9498
9559
  if (apply) {
9499
9560
  log.ok("refresh complete.");
9500
9561
  if (backupRoot) {
9501
- log.info(`Backup of overwritten templates: ${relative5(cwd, backupRoot).replace(/\\/g, "/")}`);
9562
+ log.info(`Backup of overwritten templates: ${relative7(cwd, backupRoot).replace(/\\/g, "/")}`);
9502
9563
  }
9503
9564
  log.info("Next: review changes with `git diff`, then commit:");
9504
9565
  log.dim(" git add .cdd/code-map.yml .cdd/model-policy.json specs/templates tests/templates ci-templates .github/workflows");
@@ -9527,9 +9588,9 @@ var doctor_exports = {};
9527
9588
  __export(doctor_exports, {
9528
9589
  doctor: () => doctor
9529
9590
  });
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";
9591
+ import { existsSync as existsSync18, readdirSync as readdirSync11, readFileSync as readFileSync21 } from "fs";
9592
+ import { createHash as createHash7 } from "crypto";
9593
+ import { join as join21, relative as relative8 } from "path";
9533
9594
  function fileExists(cwd, relPath) {
9534
9595
  return existsSync18(join21(cwd, relPath));
9535
9596
  }
@@ -9545,21 +9606,17 @@ function findFiles(dir, predicate, found = []) {
9545
9606
  }
9546
9607
  return found;
9547
9608
  }
9548
- function sha256OfFile3(path) {
9549
- try {
9550
- return createHash5("sha256").update(readFileSync19(path)).digest("hex");
9551
- } catch {
9552
- return "";
9553
- }
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");
9609
+ function inputDigest(paths, cwd) {
9610
+ const combined = paths.slice().sort().map((p) => {
9611
+ const rel = relative8(cwd, p).replace(/\\/g, "/");
9612
+ return `${rel}:${sha256OfFileNormalized(p)}`;
9613
+ }).join("\n");
9614
+ return createHash7("sha256").update(combined).digest("hex");
9558
9615
  }
9559
9616
  function readContextIndexMetadata(filePath) {
9560
9617
  if (!existsSync18(filePath))
9561
9618
  return {};
9562
- const text = readFileSync19(filePath, "utf8");
9619
+ const text = readFileSync21(filePath, "utf8");
9563
9620
  const out = {};
9564
9621
  const digestMatch = text.match(/^inputs-digest:\s*([a-f0-9]+)/m);
9565
9622
  if (digestMatch)
@@ -9587,7 +9644,7 @@ function checkContextFreshness(cwd) {
9587
9644
  }
9588
9645
  const projectMapMeta = readContextIndexMetadata(projectMap);
9589
9646
  const contractsIndexMeta = readContextIndexMetadata(contractsIndex);
9590
- const projectInputDigest = inputDigest([contextPolicy].filter(existsSync18));
9647
+ const projectInputDigest = inputDigest([contextPolicy].filter(existsSync18), cwd);
9591
9648
  if (projectMapMeta.inputsDigest === void 0) {
9592
9649
  findings.push({
9593
9650
  level: "warning",
@@ -9599,7 +9656,7 @@ function checkContextFreshness(cwd) {
9599
9656
  message: "specs/context/project-map.md inputs changed (.cdd/context-policy.json); re-run cdd-kit context-scan"
9600
9657
  });
9601
9658
  }
9602
- const contractsInputDigest = inputDigest(contractFiles);
9659
+ const contractsInputDigest = inputDigest(contractFiles, cwd);
9603
9660
  if (contractsIndexMeta.inputsDigest === void 0) {
9604
9661
  findings.push({
9605
9662
  level: "warning",
@@ -9624,7 +9681,7 @@ function checkContextFreshness(cwd) {
9624
9681
  }
9625
9682
  function readAgentModel(path) {
9626
9683
  try {
9627
- const text = readFileSync19(path, "utf8");
9684
+ const text = readFileSync21(path, "utf8");
9628
9685
  const m = text.match(/^model:\s*(\S+)/m);
9629
9686
  return m ? m[1] : null;
9630
9687
  } catch {
@@ -9637,7 +9694,7 @@ function checkModelPolicyDrift(cwd) {
9637
9694
  return [];
9638
9695
  let policy;
9639
9696
  try {
9640
- policy = JSON.parse(readFileSync19(policyPath, "utf8"));
9697
+ policy = JSON.parse(readFileSync21(policyPath, "utf8"));
9641
9698
  } catch {
9642
9699
  return [{ level: "warning", message: ".cdd/model-policy.json is not valid JSON" }];
9643
9700
  }
@@ -9738,7 +9795,7 @@ function checkCodeMap(cwd) {
9738
9795
  const more = probe.staleCount > 3 ? ` (+${probe.staleCount - 3} more)` : "";
9739
9796
  findings.push({ level: "warning", message: `code-map stale: ${top}${more}; run \`cdd-kit code-map\`` });
9740
9797
  }
9741
- const text = readFileSync19(mapPath, "utf8");
9798
+ const text = readFileSync21(mapPath, "utf8");
9742
9799
  const m = text.match(/^# files: (\d+), src-lines: (\d+), map-lines: (\d+), compression: ([\d.]+)x/m);
9743
9800
  if (m) {
9744
9801
  findings.push({ level: "ok", message: `code-map: ${m[1]} files, ${m[4]}x compression` });
@@ -9818,7 +9875,7 @@ async function attemptAutoFixes(cwd, report) {
9818
9875
  try {
9819
9876
  let existing = {};
9820
9877
  try {
9821
- existing = JSON.parse(readFileSync19(policyPath, "utf8"));
9878
+ existing = JSON.parse(readFileSync21(policyPath, "utf8"));
9822
9879
  } catch {
9823
9880
  }
9824
9881
  const merged = {
@@ -9911,6 +9968,7 @@ var init_doctor = __esm({
9911
9968
  init_logger();
9912
9969
  init_provider();
9913
9970
  init_freshness();
9971
+ init_digest();
9914
9972
  }
9915
9973
  });
9916
9974
 
@@ -9919,7 +9977,7 @@ var lint_agents_exports = {};
9919
9977
  __export(lint_agents_exports, {
9920
9978
  lintAgents: () => lintAgents
9921
9979
  });
9922
- import { readdirSync as readdirSync12, readFileSync as readFileSync20 } from "fs";
9980
+ import { readdirSync as readdirSync12, readFileSync as readFileSync22 } from "fs";
9923
9981
  import { join as join22 } from "path";
9924
9982
  import { load as yamlLoad2 } from "js-yaml";
9925
9983
  function extractRequiredArtifactsSection(content) {
@@ -9974,7 +10032,7 @@ async function lintAgents(opts) {
9974
10032
  const filePath = join22(agentsDir, filename);
9975
10033
  let content;
9976
10034
  try {
9977
- content = readFileSync20(filePath, "utf8");
10035
+ content = readFileSync22(filePath, "utf8");
9978
10036
  } catch {
9979
10037
  violations.push({
9980
10038
  file: filename,
@@ -10087,7 +10145,7 @@ __export(archive_exports, {
10087
10145
  archive: () => archive
10088
10146
  });
10089
10147
  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";
10148
+ import { existsSync as existsSync19, mkdirSync as mkdirSync10, renameSync as renameSync2, readFileSync as readFileSync23, writeFileSync as writeFileSync11, appendFileSync, cpSync as cpSync3, rmSync as rmSync3 } from "fs";
10091
10149
  import yaml4 from "js-yaml";
10092
10150
  async function archive(changeId) {
10093
10151
  const cwd = process.cwd();
@@ -10107,7 +10165,7 @@ async function archive(changeId) {
10107
10165
  const tasksPath = join23(changeDir, "tasks.yml");
10108
10166
  if (existsSync19(tasksPath)) {
10109
10167
  try {
10110
- const raw = readFileSync21(tasksPath, "utf8");
10168
+ const raw = readFileSync23(tasksPath, "utf8");
10111
10169
  const data = yaml4.load(raw);
10112
10170
  if (data?.status === "gate-blocked") {
10113
10171
  log.warn("tasks.yml has status: gate-blocked \u2014 archiving anyway (change was paused).");
@@ -10163,7 +10221,7 @@ __export(abandon_exports, {
10163
10221
  abandon: () => abandon
10164
10222
  });
10165
10223
  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";
10224
+ import { existsSync as existsSync20, readFileSync as readFileSync24, writeFileSync as writeFileSync12, appendFileSync as appendFileSync2, mkdirSync as mkdirSync11 } from "fs";
10167
10225
  import yaml5 from "js-yaml";
10168
10226
  async function abandon(changeId, opts) {
10169
10227
  const cwd = process.cwd();
@@ -10174,7 +10232,7 @@ async function abandon(changeId, opts) {
10174
10232
  process.exit(1);
10175
10233
  }
10176
10234
  if (existsSync20(tasksPath)) {
10177
- const raw = readFileSync22(tasksPath, "utf8");
10235
+ const raw = readFileSync24(tasksPath, "utf8");
10178
10236
  const data = yaml5.load(raw) ?? {};
10179
10237
  data["status"] = "abandoned";
10180
10238
  if (!data["change-id"]) {
@@ -10217,7 +10275,7 @@ __export(list_changes_exports, {
10217
10275
  listChanges: () => listChanges
10218
10276
  });
10219
10277
  import { join as join25 } from "path";
10220
- import { existsSync as existsSync21, readdirSync as readdirSync13, readFileSync as readFileSync23 } from "fs";
10278
+ import { existsSync as existsSync21, readdirSync as readdirSync13, readFileSync as readFileSync25 } from "fs";
10221
10279
  import yaml6 from "js-yaml";
10222
10280
  async function listChanges() {
10223
10281
  const cwd = process.cwd();
@@ -10237,7 +10295,7 @@ async function listChanges() {
10237
10295
  let pending = 0;
10238
10296
  if (existsSync21(tasksPath)) {
10239
10297
  try {
10240
- const raw = readFileSync23(tasksPath, "utf8");
10298
+ const raw = readFileSync25(tasksPath, "utf8");
10241
10299
  const data = yaml6.load(raw);
10242
10300
  if (data?.status)
10243
10301
  status = data.status;
@@ -10268,7 +10326,7 @@ __export(context_exports, {
10268
10326
  rejectContextExpansion: () => rejectContextExpansion,
10269
10327
  requestContextExpansion: () => requestContextExpansion
10270
10328
  });
10271
- import { existsSync as existsSync22, readFileSync as readFileSync24, writeFileSync as writeFileSync13 } from "fs";
10329
+ import { existsSync as existsSync22, readFileSync as readFileSync26, writeFileSync as writeFileSync13 } from "fs";
10272
10330
  import { join as join26 } from "path";
10273
10331
  function normalizePath(path) {
10274
10332
  return path.replace(/\\/g, "/").replace(/^\.\//, "").trim();
@@ -10291,7 +10349,7 @@ function readManifest(changeId) {
10291
10349
  log.error(`context manifest not found: specs/changes/${changeId}/context-manifest.md`);
10292
10350
  process.exit(1);
10293
10351
  }
10294
- return readFileSync24(manifestPath, "utf8");
10352
+ return readFileSync26(manifestPath, "utf8");
10295
10353
  }
10296
10354
  function writeManifest(changeId, content) {
10297
10355
  writeFileSync13(manifestPathFor(changeId), content.endsWith("\n") ? content : `${content}
@@ -10519,7 +10577,7 @@ var init_context = __esm({
10519
10577
  });
10520
10578
 
10521
10579
  // src/cli/index.ts
10522
- import { readFileSync as readFileSync25 } from "fs";
10580
+ import { readFileSync as readFileSync27 } from "fs";
10523
10581
  import { fileURLToPath as fileURLToPath3 } from "url";
10524
10582
  import { dirname as dirname7, join as join27 } from "path";
10525
10583
  import { Command } from "commander";
@@ -10941,22 +10999,19 @@ init_update();
10941
10999
 
10942
11000
  // src/commands/new-change.ts
10943
11001
  init_paths();
10944
- import { join as join9 } from "path";
10945
- import { createHash as createHash3 } from "crypto";
10946
- import { existsSync as existsSync8, readFileSync as readFileSync7, readdirSync as readdirSync5, writeFileSync as writeFileSync4 } from "fs";
11002
+ import { join as join9, relative as relative3 } from "path";
11003
+ import { createHash as createHash4 } from "crypto";
11004
+ import { existsSync as existsSync8, readFileSync as readFileSync8, readdirSync as readdirSync5, writeFileSync as writeFileSync4 } from "fs";
10947
11005
  import yaml from "js-yaml";
10948
11006
  init_logger();
10949
11007
  init_context_scan();
10950
- function sha256OfFile2(path) {
10951
- try {
10952
- return createHash3("sha256").update(readFileSync7(path)).digest("hex");
10953
- } catch {
10954
- return "";
10955
- }
10956
- }
10957
- function inputsDigest2(paths) {
10958
- const combined = paths.slice().sort().map((p) => `${p}:${sha256OfFile2(p)}`).join("\n");
10959
- return createHash3("sha256").update(combined).digest("hex");
11008
+ init_digest();
11009
+ function inputsDigest2(paths, cwd) {
11010
+ const combined = paths.slice().sort().map((p) => {
11011
+ const rel = relative3(cwd, p).replace(/\\/g, "/");
11012
+ return `${rel}:${sha256OfFileNormalized(p)}`;
11013
+ }).join("\n");
11014
+ return createHash4("sha256").update(combined).digest("hex");
10960
11015
  }
10961
11016
  function findContractFiles2(dir, found = []) {
10962
11017
  if (!existsSync8(dir))
@@ -10974,7 +11029,7 @@ function findContractFiles2(dir, found = []) {
10974
11029
  function readIndexDigest(filePath) {
10975
11030
  if (!existsSync8(filePath))
10976
11031
  return null;
10977
- const m = readFileSync7(filePath, "utf8").match(/^inputs-digest:\s*([a-f0-9]+)/m);
11032
+ const m = readFileSync8(filePath, "utf8").match(/^inputs-digest:\s*([a-f0-9]+)/m);
10978
11033
  return m ? m[1] : null;
10979
11034
  }
10980
11035
  async function ensureFreshContextIndexes(cwd) {
@@ -10983,8 +11038,8 @@ async function ensureFreshContextIndexes(cwd) {
10983
11038
  const policyPath = join9(cwd, ".cdd", "context-policy.json");
10984
11039
  const policyInputs = [policyPath].filter(existsSync8);
10985
11040
  const contractFiles = findContractFiles2(join9(cwd, "contracts"));
10986
- const wantProjectDigest = inputsDigest2(policyInputs);
10987
- const wantContractsDigest = inputsDigest2(contractFiles);
11041
+ const wantProjectDigest = inputsDigest2(policyInputs, cwd);
11042
+ const wantContractsDigest = inputsDigest2(contractFiles, cwd);
10988
11043
  const haveProjectDigest = readIndexDigest(projectMap);
10989
11044
  const haveContractsDigest = readIndexDigest(contractsIndex);
10990
11045
  const needsScan = !existsSync8(projectMap) || !existsSync8(contractsIndex) || haveProjectDigest !== wantProjectDigest || haveContractsDigest !== wantContractsDigest;
@@ -11066,7 +11121,7 @@ async function newChange(name, opts) {
11066
11121
  if (dependencies.length > 0) {
11067
11122
  const tasksPath = join9(changeDir, "tasks.yml");
11068
11123
  if (existsSync8(tasksPath)) {
11069
- const raw = readFileSync7(tasksPath, "utf8");
11124
+ const raw = readFileSync8(tasksPath, "utf8");
11070
11125
  const data = yaml.load(raw) ?? {};
11071
11126
  data["depends-on"] = dependencies;
11072
11127
  writeFileSync4(tasksPath, yaml.dump(data, { lineWidth: -1, noRefs: true }), "utf8");
@@ -11170,9 +11225,9 @@ async function validate(opts) {
11170
11225
  var import_ajv = __toESM(require_ajv(), 1);
11171
11226
  var import_ajv_formats = __toESM(require_dist(), 1);
11172
11227
  init_logger();
11173
- import { existsSync as existsSync12, readFileSync as readFileSync9, readdirSync as readdirSync7 } from "fs";
11228
+ import { existsSync as existsSync13, readFileSync as readFileSync16, readdirSync as readdirSync7 } from "fs";
11174
11229
  import { homedir as homedir3 } from "os";
11175
- import { join as join14 } from "path";
11230
+ import { join as join16 } from "path";
11176
11231
  import yaml2 from "js-yaml";
11177
11232
  import picomatch2 from "picomatch";
11178
11233
 
@@ -11350,15 +11405,15 @@ function parseContextManifest(content) {
11350
11405
  }
11351
11406
  function extractRequiredArtifactTypes(cwd, agentName) {
11352
11407
  const candidates = [
11353
- join14(cwd, ".claude", "agents", `${agentName}.md`),
11354
- join14(homedir3(), ".claude", "agents", `${agentName}.md`)
11408
+ join16(cwd, ".claude", "agents", `${agentName}.md`),
11409
+ join16(homedir3(), ".claude", "agents", `${agentName}.md`)
11355
11410
  ];
11356
11411
  let content = null;
11357
11412
  for (const candidate of candidates) {
11358
- if (!existsSync12(candidate))
11413
+ if (!existsSync13(candidate))
11359
11414
  continue;
11360
11415
  try {
11361
- content = readFileSync9(candidate, "utf8");
11416
+ content = readFileSync16(candidate, "utf8");
11362
11417
  break;
11363
11418
  } catch {
11364
11419
  }
@@ -11395,11 +11450,11 @@ function loadContextPolicy(cwd) {
11395
11450
  unknownFilesRead: "warn-for-legacy-fail-for-new"
11396
11451
  }
11397
11452
  };
11398
- const policyPath = join14(cwd, ".cdd", "context-policy.json");
11399
- if (!existsSync12(policyPath))
11453
+ const policyPath = join16(cwd, ".cdd", "context-policy.json");
11454
+ if (!existsSync13(policyPath))
11400
11455
  return defaults;
11401
11456
  try {
11402
- const custom = JSON.parse(readFileSync9(policyPath, "utf8"));
11457
+ const custom = JSON.parse(readFileSync16(policyPath, "utf8"));
11403
11458
  return {
11404
11459
  ...defaults,
11405
11460
  ...custom,
@@ -11413,7 +11468,7 @@ function loadContextPolicy(cwd) {
11413
11468
  }
11414
11469
  function loadYamlFile(path) {
11415
11470
  try {
11416
- const raw = readFileSync9(path, "utf8");
11471
+ const raw = readFileSync16(path, "utf8");
11417
11472
  return { data: yaml2.load(raw), parseError: null };
11418
11473
  } catch (err) {
11419
11474
  return { data: null, parseError: err.message };
@@ -11444,8 +11499,8 @@ function ajvErrorsToMessages(errors, prefix, knownKeys) {
11444
11499
  return out;
11445
11500
  }
11446
11501
  function isContextGovernedChange(changeDir) {
11447
- const tasksPath = join14(changeDir, "tasks.yml");
11448
- if (!existsSync12(tasksPath))
11502
+ const tasksPath = join16(changeDir, "tasks.yml");
11503
+ if (!existsSync13(tasksPath))
11449
11504
  return false;
11450
11505
  const { data } = loadYamlFile(tasksPath);
11451
11506
  return data?.["context-governance"] === "v1";
@@ -11473,12 +11528,12 @@ function lintTasksFile(tasksPath, errors, warnings) {
11473
11528
  return data;
11474
11529
  }
11475
11530
  function resolveTier(changeDir) {
11476
- const classifPath = join14(changeDir, "change-classification.md");
11477
- const classificationPresent = existsSync12(classifPath);
11478
- const classificationText = classificationPresent ? readFileSync9(classifPath, "utf8") : "";
11531
+ const classifPath = join16(changeDir, "change-classification.md");
11532
+ const classificationPresent = existsSync13(classifPath);
11533
+ const classificationText = classificationPresent ? readFileSync16(classifPath, "utf8") : "";
11479
11534
  const classificationHasLooseMarker = classificationPresent && TIER_PATTERN.test(classificationText);
11480
- const tasksPath = join14(changeDir, "tasks.yml");
11481
- if (existsSync12(tasksPath)) {
11535
+ const tasksPath = join16(changeDir, "tasks.yml");
11536
+ if (existsSync13(tasksPath)) {
11482
11537
  const { data } = loadYamlFile(tasksPath);
11483
11538
  const t = data?.tier;
11484
11539
  if (typeof t === "number" && t >= 0 && t <= 5) {
@@ -11524,7 +11579,7 @@ function enforceTierRequirements(changeDir, agentLogDir, errors, warnings) {
11524
11579
  return;
11525
11580
  }
11526
11581
  const tier = resolution.tier;
11527
- const agentLogFiles = agentLogDir && existsSync12(agentLogDir) ? readdirSync7(agentLogDir).map((f) => f.replace(/\.ya?ml$/, "")) : [];
11582
+ const agentLogFiles = agentLogDir && existsSync13(agentLogDir) ? readdirSync7(agentLogDir).map((f) => f.replace(/\.ya?ml$/, "")) : [];
11528
11583
  if (tier <= 1) {
11529
11584
  for (const required of ["e2e-resilience-engineer", "monkey-test-engineer", "stress-soak-engineer"]) {
11530
11585
  if (!agentLogFiles.includes(required)) {
@@ -11540,7 +11595,7 @@ function enforceTierRequirements(changeDir, agentLogDir, errors, warnings) {
11540
11595
  }
11541
11596
  }
11542
11597
  if (resolution.source === "tasks-frontmatter" && resolution.classificationPresent) {
11543
- const text = readFileSync9(join14(changeDir, "change-classification.md"), "utf8");
11598
+ const text = readFileSync16(join16(changeDir, "change-classification.md"), "utf8");
11544
11599
  const structured = text.match(/^## Tier\s*\n\s*-\s*(\d)\s*$/m);
11545
11600
  const bold = text.match(/\*\*Tier:\*\*\s*Tier\s*(\d)\b/i);
11546
11601
  const classifTier = structured ? parseInt(structured[1], 10) : bold ? parseInt(bold[1], 10) : NaN;
@@ -11552,11 +11607,11 @@ function enforceTierRequirements(changeDir, agentLogDir, errors, warnings) {
11552
11607
  }
11553
11608
  }
11554
11609
  function isArchivedChange(cwd, changeId) {
11555
- const archiveRoot = join14(cwd, "specs", "archive");
11556
- if (!existsSync12(archiveRoot))
11610
+ const archiveRoot = join16(cwd, "specs", "archive");
11611
+ if (!existsSync13(archiveRoot))
11557
11612
  return false;
11558
11613
  const years = readdirSync7(archiveRoot, { withFileTypes: true }).filter((d) => d.isDirectory());
11559
- return years.some((year) => existsSync12(join14(archiveRoot, year.name, changeId)));
11614
+ return years.some((year) => existsSync13(join16(archiveRoot, year.name, changeId)));
11560
11615
  }
11561
11616
  function detectDependencyCycle(cwd, startChangeId) {
11562
11617
  const visited = /* @__PURE__ */ new Set();
@@ -11569,8 +11624,8 @@ function detectDependencyCycle(cwd, startChangeId) {
11569
11624
  return null;
11570
11625
  visited.add(id);
11571
11626
  stack.push(id);
11572
- const tasksPath = join14(cwd, "specs", "changes", id, "tasks.yml");
11573
- if (existsSync12(tasksPath)) {
11627
+ const tasksPath = join16(cwd, "specs", "changes", id, "tasks.yml");
11628
+ if (existsSync13(tasksPath)) {
11574
11629
  const { data } = loadYamlFile(tasksPath);
11575
11630
  const deps = data?.["depends-on"] ?? [];
11576
11631
  for (const dep of deps) {
@@ -11585,8 +11640,8 @@ function detectDependencyCycle(cwd, startChangeId) {
11585
11640
  return visit(startChangeId);
11586
11641
  }
11587
11642
  function validateDependencies(cwd, changeId, changeDir) {
11588
- const tasksPath = join14(changeDir, "tasks.yml");
11589
- if (!existsSync12(tasksPath))
11643
+ const tasksPath = join16(changeDir, "tasks.yml");
11644
+ if (!existsSync13(tasksPath))
11590
11645
  return [];
11591
11646
  const { data } = loadYamlFile(tasksPath);
11592
11647
  const dependencies = data?.["depends-on"] ?? [];
@@ -11600,10 +11655,10 @@ function validateDependencies(cwd, changeId, changeDir) {
11600
11655
  errors.push(`tasks.yml: change cannot depend on itself (${dep})`);
11601
11656
  continue;
11602
11657
  }
11603
- const upstreamDir = join14(cwd, "specs", "changes", dep);
11604
- if (existsSync12(upstreamDir)) {
11605
- const upstreamTasks = join14(upstreamDir, "tasks.yml");
11606
- if (!existsSync12(upstreamTasks)) {
11658
+ const upstreamDir = join16(cwd, "specs", "changes", dep);
11659
+ if (existsSync13(upstreamDir)) {
11660
+ const upstreamTasks = join16(upstreamDir, "tasks.yml");
11661
+ if (!existsSync13(upstreamTasks)) {
11607
11662
  errors.push(`dependency ${dep}: missing tasks.yml`);
11608
11663
  continue;
11609
11664
  }
@@ -11624,8 +11679,8 @@ async function gate(changeId, opts = {}) {
11624
11679
  const strict = opts.strict ?? false;
11625
11680
  const lax = opts.lax ?? false;
11626
11681
  const cwd = process.cwd();
11627
- const changeDir = join14(cwd, "specs", "changes", changeId);
11628
- if (!existsSync12(changeDir)) {
11682
+ const changeDir = join16(cwd, "specs", "changes", changeId);
11683
+ if (!existsSync13(changeDir)) {
11629
11684
  log.error(`change not found: ${changeId} (looked in ${changeDir})`);
11630
11685
  process.exit(1);
11631
11686
  }
@@ -11651,13 +11706,13 @@ async function gate(changeId, opts = {}) {
11651
11706
  }
11652
11707
  const contextPolicy = loadContextPolicy(cwd);
11653
11708
  const isNewChange = isContextGovernedChange(changeDir);
11654
- const manifestPath = join14(changeDir, "context-manifest.md");
11655
- const hasManifest = existsSync12(manifestPath);
11709
+ const manifestPath = join16(changeDir, "context-manifest.md");
11710
+ const hasManifest = existsSync13(manifestPath);
11656
11711
  let allowedPaths = [];
11657
11712
  let approvedExpansions = [];
11658
11713
  errors.push(...validateDependencies(cwd, changeId, changeDir));
11659
11714
  if (hasManifest) {
11660
- const manifest = parseContextManifest(readFileSync9(manifestPath, "utf8"));
11715
+ const manifest = parseContextManifest(readFileSync16(manifestPath, "utf8"));
11661
11716
  allowedPaths = manifest.allowedPaths;
11662
11717
  approvedExpansions = manifest.approvedExpansions;
11663
11718
  if (manifest.pendingExpansions > 0) {
@@ -11675,7 +11730,7 @@ async function gate(changeId, opts = {}) {
11675
11730
  }
11676
11731
  continue;
11677
11732
  }
11678
- if (!existsSync12(join14(changeDir, f))) {
11733
+ if (!existsSync13(join16(changeDir, f))) {
11679
11734
  errors.push(`missing required artifact: ${f}`);
11680
11735
  }
11681
11736
  }
@@ -11685,21 +11740,21 @@ async function gate(changeId, opts = {}) {
11685
11740
  continue;
11686
11741
  if (f === "tasks.yml")
11687
11742
  continue;
11688
- const content = readFileSync9(join14(changeDir, f), "utf8");
11743
+ const content = readFileSync16(join16(changeDir, f), "utf8");
11689
11744
  const minChars = MIN_CHARS[f] ?? 100;
11690
11745
  if (meaningfulChars(content) < minChars) {
11691
11746
  errors.push(`${f}: appears to be a stub (< ${minChars} meaningful chars)`);
11692
11747
  }
11693
11748
  }
11694
- const classifPath = join14(changeDir, "change-classification.md");
11749
+ const classifPath = join16(changeDir, "change-classification.md");
11695
11750
  const tierResolution = resolveTier(changeDir);
11696
- if (tierResolution.tier === null && existsSync12(classifPath) && !tierResolution.classificationHasLooseMarker) {
11751
+ if (tierResolution.tier === null && existsSync13(classifPath) && !tierResolution.classificationHasLooseMarker) {
11697
11752
  errors.push("change-classification.md: missing tier/risk marker (set tier in tasks.yml frontmatter, or include Tier 0-5 / low|medium|high|critical in change-classification.md)");
11698
11753
  }
11699
11754
  }
11700
- const tasksPath = join14(changeDir, "tasks.yml");
11755
+ const tasksPath = join16(changeDir, "tasks.yml");
11701
11756
  let tasksData = null;
11702
- if (existsSync12(tasksPath)) {
11757
+ if (existsSync13(tasksPath)) {
11703
11758
  tasksData = lintTasksFile(tasksPath, errors, warnings);
11704
11759
  }
11705
11760
  if (tasksData) {
@@ -11713,11 +11768,11 @@ async function gate(changeId, opts = {}) {
11713
11768
  }
11714
11769
  }
11715
11770
  }
11716
- const agentLogDir = join14(changeDir, "agent-log");
11717
- if (existsSync12(agentLogDir)) {
11771
+ const agentLogDir = join16(changeDir, "agent-log");
11772
+ if (existsSync13(agentLogDir)) {
11718
11773
  const logFiles = readdirSync7(agentLogDir).filter((f) => f.endsWith(".yml"));
11719
11774
  for (const f of logFiles) {
11720
- const fullPath = join14(agentLogDir, f);
11775
+ const fullPath = join16(agentLogDir, f);
11721
11776
  const { data, parseError } = loadYamlFile(fullPath);
11722
11777
  if (parseError) {
11723
11778
  errors.push(`agent-log/${f}: invalid YAML: ${parseError}`);
@@ -11785,9 +11840,9 @@ async function gate(changeId, opts = {}) {
11785
11840
  errors.push(`agent-log/${f}: read unauthorized path -> ${p} (not in allowed paths or approved expansions)`);
11786
11841
  }
11787
11842
  }
11788
- const runtimeLog = join14(cwd, ".cdd", "runtime", `${changeId}-files-read.jsonl`);
11789
- if (existsSync12(runtimeLog)) {
11790
- const runtimePaths = readFileSync9(runtimeLog, "utf8").split("\n").filter(Boolean).map((line) => {
11843
+ const runtimeLog = join16(cwd, ".cdd", "runtime", `${changeId}-files-read.jsonl`);
11844
+ if (existsSync13(runtimeLog)) {
11845
+ const runtimePaths = readFileSync16(runtimeLog, "utf8").split("\n").filter(Boolean).map((line) => {
11791
11846
  try {
11792
11847
  return JSON.parse(line).path;
11793
11848
  } catch {
@@ -11823,8 +11878,8 @@ async function gate(changeId, opts = {}) {
11823
11878
  continue;
11824
11879
  const pathPart = pointer.split(":")[0];
11825
11880
  if (pathPart.includes("/") && !pointer.startsWith("http")) {
11826
- const abs = join14(cwd, pathPart);
11827
- if (!existsSync12(abs)) {
11881
+ const abs = join16(cwd, pathPart);
11882
+ if (!existsSync13(abs)) {
11828
11883
  errors.push(`agent-log/${f}: artifact pointer not found: ${pathPart}`);
11829
11884
  }
11830
11885
  }
@@ -11873,26 +11928,26 @@ async function gate(changeId, opts = {}) {
11873
11928
  // src/commands/install-hooks.ts
11874
11929
  init_paths();
11875
11930
  init_logger();
11876
- import { existsSync as existsSync13, readFileSync as readFileSync10, writeFileSync as writeFileSync5, chmodSync as chmodSync2, mkdirSync as mkdirSync5 } from "fs";
11877
- import { join as join15 } from "path";
11931
+ import { existsSync as existsSync14, readFileSync as readFileSync17, writeFileSync as writeFileSync7, chmodSync as chmodSync2, mkdirSync as mkdirSync6 } from "fs";
11932
+ import { join as join17 } from "path";
11878
11933
  var START_MARKER2 = "# cdd-kit-managed-block-start";
11879
11934
  var END_MARKER2 = "# cdd-kit-managed-block-end";
11880
11935
  async function installHooks() {
11881
11936
  const cwd = process.cwd();
11882
- const gitDir = join15(cwd, ".git");
11883
- if (!existsSync13(gitDir)) {
11937
+ const gitDir = join17(cwd, ".git");
11938
+ if (!existsSync14(gitDir)) {
11884
11939
  log.error("not a git repository (no .git/ found in cwd)");
11885
11940
  process.exit(1);
11886
11941
  }
11887
- const hooksDir = join15(gitDir, "hooks");
11888
- mkdirSync5(hooksDir, { recursive: true });
11889
- const dest = join15(hooksDir, "pre-commit");
11890
- const ourHook = readFileSync10(join15(ASSET.hooks, "pre-commit"), "utf8");
11942
+ const hooksDir = join17(gitDir, "hooks");
11943
+ mkdirSync6(hooksDir, { recursive: true });
11944
+ const dest = join17(hooksDir, "pre-commit");
11945
+ const ourHook = readFileSync17(join17(ASSET.hooks, "pre-commit"), "utf8");
11891
11946
  let final;
11892
- if (!existsSync13(dest)) {
11947
+ if (!existsSync14(dest)) {
11893
11948
  final = ourHook;
11894
11949
  } else {
11895
- const existing = readFileSync10(dest, "utf8");
11950
+ const existing = readFileSync17(dest, "utf8");
11896
11951
  const startIdx = existing.indexOf(START_MARKER2);
11897
11952
  const endIdx = existing.indexOf(END_MARKER2);
11898
11953
  if (startIdx >= 0 && endIdx > startIdx) {
@@ -11916,7 +11971,7 @@ async function installHooks() {
11916
11971
  }
11917
11972
  }
11918
11973
  }
11919
- writeFileSync5(dest, final, "utf8");
11974
+ writeFileSync7(dest, final, "utf8");
11920
11975
  try {
11921
11976
  chmodSync2(dest, 493);
11922
11977
  } catch {
@@ -11927,7 +11982,7 @@ async function installHooks() {
11927
11982
 
11928
11983
  // src/cli/index.ts
11929
11984
  var __dirname2 = dirname7(fileURLToPath3(import.meta.url));
11930
- var pkg = JSON.parse(readFileSync25(join27(__dirname2, "..", "..", "package.json"), "utf8"));
11985
+ var pkg = JSON.parse(readFileSync27(join27(__dirname2, "..", "..", "package.json"), "utf8"));
11931
11986
  var program = new Command();
11932
11987
  program.name("cdd-kit").description("Contract-Driven Delivery Kit CLI").version(pkg.version);
11933
11988
  program.command("init").description(