contract-driven-delivery 2.0.8 → 2.0.10

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 +101 -0
  2. package/dist/cli/index.js +1666 -1523
  3. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -391,8 +391,11 @@ function sha256OfFile(path) {
391
391
  return "";
392
392
  }
393
393
  }
394
- function inputsDigest(paths) {
395
- const combined = paths.slice().sort().map((p) => `${p}:${sha256OfFile(p)}`).join("\n");
394
+ function inputsDigest(paths, cwd) {
395
+ const combined = paths.slice().sort().map((p) => {
396
+ const rel = relative2(cwd, p).replace(/\\/g, "/");
397
+ return `${rel}:${sha256OfFile(p)}`;
398
+ }).join("\n");
396
399
  return createHash2("sha256").update(combined).digest("hex");
397
400
  }
398
401
  function stripGlobSuffix(pattern) {
@@ -415,7 +418,14 @@ function getForbiddenPaths(cwd) {
415
418
  }
416
419
  function isForbidden(relPath, forbidden) {
417
420
  const normalized = relPath.replace(/\\/g, "/");
418
- return forbidden.some((pattern) => normalized === pattern || normalized.startsWith(`${pattern}/`));
421
+ if (forbidden.some((pattern) => normalized === pattern || normalized.startsWith(`${pattern}/`))) {
422
+ return true;
423
+ }
424
+ for (const segment of normalized.split("/")) {
425
+ if (FORBIDDEN_DIRECTORY_NAMES.has(segment))
426
+ return true;
427
+ }
428
+ return false;
419
429
  }
420
430
  function buildTree(dir, cwd, forbidden, stats, prefix = "", depth = 0) {
421
431
  const entries = readdirSync4(dir, { withFileTypes: true }).sort((a, b) => {
@@ -542,7 +552,7 @@ async function contextScan(opts = {}) {
542
552
  `visible-files: ${treeStats.files}`,
543
553
  `omitted-dirs: ${treeStats.omittedDirs}`,
544
554
  `truncated-dirs: ${treeStats.truncatedDirs}`,
545
- `inputs-digest: ${inputsDigest(projectMapInputs)}`,
555
+ `inputs-digest: ${inputsDigest(projectMapInputs, cwd)}`,
546
556
  "---",
547
557
  "",
548
558
  "# Project Map",
@@ -608,7 +618,7 @@ async function contextScan(opts = {}) {
608
618
  "schema-version: 1",
609
619
  `contract-count: ${contractFiles.length}`,
610
620
  `missing-summary-count: ${missingSummary}`,
611
- `inputs-digest: ${inputsDigest(contractFiles)}`,
621
+ `inputs-digest: ${inputsDigest(contractFiles, cwd)}`,
612
622
  "---",
613
623
  "",
614
624
  "# Contracts Index",
@@ -632,7 +642,7 @@ async function contextScan(opts = {}) {
632
642
  log.ok("Created specs/context/contracts-index.md");
633
643
  }
634
644
  }
635
- var DEFAULT_FORBIDDEN, PER_DIR_ENTRY_CAP;
645
+ var DEFAULT_FORBIDDEN, FORBIDDEN_DIRECTORY_NAMES, PER_DIR_ENTRY_CAP;
636
646
  var init_context_scan = __esm({
637
647
  "src/commands/context-scan.ts"() {
638
648
  "use strict";
@@ -645,8 +655,47 @@ var init_context_scan = __esm({
645
655
  "build",
646
656
  "assets",
647
657
  "specs/archive",
648
- "specs/changes"
658
+ "specs/changes",
659
+ // cdd-kit runtime artifacts. Without these the user's local backups land
660
+ // in `specs/context/project-map.md`, polluting it and breaking the
661
+ // inputs-digest match for any fresh clone.
662
+ ".cdd/.refresh-backup",
663
+ ".cdd/migrate-backup",
664
+ ".cdd/runtime"
649
665
  ];
666
+ FORBIDDEN_DIRECTORY_NAMES = /* @__PURE__ */ new Set([
667
+ // Node / generic build outputs — also in DEFAULT_FORBIDDEN at top level,
668
+ // but nested cases like `frontend/dist`, `apps/foo/build`, `packages/x/out`
669
+ // require basename matching.
670
+ "node_modules",
671
+ "dist",
672
+ "build",
673
+ "out",
674
+ // Python
675
+ "__pycache__",
676
+ ".pytest_cache",
677
+ ".mypy_cache",
678
+ ".ruff_cache",
679
+ ".tox",
680
+ // JS/TS frameworks
681
+ ".next",
682
+ ".nuxt",
683
+ ".svelte-kit",
684
+ ".parcel-cache",
685
+ ".turbo",
686
+ ".nyc_output",
687
+ // Generic build / coverage caches
688
+ "coverage",
689
+ "htmlcov",
690
+ ".cache",
691
+ // Virtualenvs
692
+ "venv",
693
+ ".venv",
694
+ // IDE / OS noise
695
+ ".idea",
696
+ ".vscode",
697
+ ".DS_Store"
698
+ ]);
650
699
  PER_DIR_ENTRY_CAP = 50;
651
700
  }
652
701
  });
@@ -4221,49 +4270,49 @@ var require_fast_uri = __commonJS({
4221
4270
  schemelessOptions.skipEscape = true;
4222
4271
  return serialize(resolved, schemelessOptions);
4223
4272
  }
4224
- function resolveComponent(base, relative6, options, skipNormalization) {
4273
+ function resolveComponent(base, relative9, options, skipNormalization) {
4225
4274
  const target = {};
4226
4275
  if (!skipNormalization) {
4227
4276
  base = parse3(serialize(base, options), options);
4228
- relative6 = parse3(serialize(relative6, options), options);
4277
+ relative9 = parse3(serialize(relative9, options), options);
4229
4278
  }
4230
4279
  options = options || {};
4231
- if (!options.tolerant && relative6.scheme) {
4232
- target.scheme = relative6.scheme;
4233
- target.userinfo = relative6.userinfo;
4234
- target.host = relative6.host;
4235
- target.port = relative6.port;
4236
- target.path = removeDotSegments(relative6.path || "");
4237
- target.query = relative6.query;
4280
+ if (!options.tolerant && relative9.scheme) {
4281
+ target.scheme = relative9.scheme;
4282
+ target.userinfo = relative9.userinfo;
4283
+ target.host = relative9.host;
4284
+ target.port = relative9.port;
4285
+ target.path = removeDotSegments(relative9.path || "");
4286
+ target.query = relative9.query;
4238
4287
  } else {
4239
- if (relative6.userinfo !== void 0 || relative6.host !== void 0 || relative6.port !== void 0) {
4240
- target.userinfo = relative6.userinfo;
4241
- target.host = relative6.host;
4242
- target.port = relative6.port;
4243
- target.path = removeDotSegments(relative6.path || "");
4244
- target.query = relative6.query;
4288
+ if (relative9.userinfo !== void 0 || relative9.host !== void 0 || relative9.port !== void 0) {
4289
+ target.userinfo = relative9.userinfo;
4290
+ target.host = relative9.host;
4291
+ target.port = relative9.port;
4292
+ target.path = removeDotSegments(relative9.path || "");
4293
+ target.query = relative9.query;
4245
4294
  } else {
4246
- if (!relative6.path) {
4295
+ if (!relative9.path) {
4247
4296
  target.path = base.path;
4248
- if (relative6.query !== void 0) {
4249
- target.query = relative6.query;
4297
+ if (relative9.query !== void 0) {
4298
+ target.query = relative9.query;
4250
4299
  } else {
4251
4300
  target.query = base.query;
4252
4301
  }
4253
4302
  } else {
4254
- if (relative6.path[0] === "/") {
4255
- target.path = removeDotSegments(relative6.path);
4303
+ if (relative9.path[0] === "/") {
4304
+ target.path = removeDotSegments(relative9.path);
4256
4305
  } else {
4257
4306
  if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) {
4258
- target.path = "/" + relative6.path;
4307
+ target.path = "/" + relative9.path;
4259
4308
  } else if (!base.path) {
4260
- target.path = relative6.path;
4309
+ target.path = relative9.path;
4261
4310
  } else {
4262
- target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative6.path;
4311
+ target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative9.path;
4263
4312
  }
4264
4313
  target.path = removeDotSegments(target.path);
4265
4314
  }
4266
- target.query = relative6.query;
4315
+ target.query = relative9.query;
4267
4316
  }
4268
4317
  target.userinfo = base.userinfo;
4269
4318
  target.host = base.host;
@@ -4271,7 +4320,7 @@ var require_fast_uri = __commonJS({
4271
4320
  }
4272
4321
  target.scheme = base.scheme;
4273
4322
  }
4274
- target.fragment = relative6.fragment;
4323
+ target.fragment = relative9.fragment;
4275
4324
  return target;
4276
4325
  }
4277
4326
  function equal(uriA, uriB, options) {
@@ -7583,1545 +7632,1609 @@ var init_include_exclude = __esm({
7583
7632
  }
7584
7633
  });
7585
7634
 
7586
- // src/code-map/freshness.ts
7587
- var freshness_exports = {};
7588
- __export(freshness_exports, {
7589
- checkCodeMapFreshness: () => checkCodeMapFreshness
7590
- });
7591
- import { existsSync as existsSync11, statSync as statSync3 } from "fs";
7592
- import { join as join13 } from "path";
7593
- function checkCodeMapFreshness(cwd, mapRel = ".cdd/code-map.yml", include, exclude) {
7594
- const mapPath = join13(cwd, mapRel);
7595
- let cfg;
7596
- try {
7597
- cfg = loadCodeMapConfig(cwd);
7598
- } catch (err) {
7599
- return {
7600
- status: "config-error",
7601
- staleFiles: [],
7602
- staleCount: 0,
7603
- mapPath,
7604
- configError: err.message
7605
- };
7635
+ // src/code-map/yaml-writer.ts
7636
+ function quoteScalar(s) {
7637
+ if (s.startsWith(".") || YAML_RESERVED.test(s) || s.includes(" ") || s === "" || s === "null" || s === "true" || s === "false") {
7638
+ return `'${s.replace(/'/g, "''")}'`;
7606
7639
  }
7607
- const includeFinal = [...cfg.include, ...include ?? []];
7608
- const excludeFinal = [...cfg.exclude, ...exclude ?? []];
7609
- const sourceFiles = walkRepo(cwd, { include: includeFinal, exclude: excludeFinal });
7610
- if (!existsSync11(mapPath)) {
7611
- if (sourceFiles.length === 0) {
7612
- return { status: "missing-greenfield", staleFiles: [], staleCount: 0, mapPath };
7640
+ return s;
7641
+ }
7642
+ function quotePath(p) {
7643
+ if (YAML_RESERVED.test(p) || p.includes(" ")) {
7644
+ return `'${p.replace(/'/g, "''")}'`;
7645
+ }
7646
+ return p;
7647
+ }
7648
+ function renderItems(items) {
7649
+ if (items.length === 0)
7650
+ return "[]";
7651
+ return `[${items.map(quoteScalar).join(", ")}]`;
7652
+ }
7653
+ function truncateDecorator(d, max = 80) {
7654
+ const cleaned = d.replace(/\r?\n/g, " ");
7655
+ return cleaned.length <= max ? cleaned : cleaned.slice(0, max) + "...";
7656
+ }
7657
+ function renderClass(c) {
7658
+ const lines = [];
7659
+ lines.push(` - name: ${c.name}`);
7660
+ lines.push(` lines: ${c.lines[0]}-${c.lines[1]}`);
7661
+ if (c.methods.length > 0) {
7662
+ lines.push(" methods:");
7663
+ for (const m of c.methods) {
7664
+ const asyncPrefix = m.async ? "async " : "";
7665
+ lines.push(` - { name: ${asyncPrefix}${m.name}, lines: ${m.lines[0]}-${m.lines[1]} }`);
7613
7666
  }
7614
- return { status: "missing-with-sources", staleFiles: [], staleCount: 0, mapPath };
7615
7667
  }
7616
- const mapMtime = statSync3(mapPath).mtimeMs;
7617
- const staleAll = [];
7618
- for (const absPath of sourceFiles) {
7668
+ return lines;
7669
+ }
7670
+ function renderFunction(f) {
7671
+ const asyncPrefix = f.async ? "async " : "";
7672
+ let comment = "";
7673
+ if (f.decorators.length > 0) {
7674
+ const truncated = truncateDecorator(f.decorators[0]);
7675
+ comment = ` # @${truncated}`;
7676
+ }
7677
+ return ` - { name: ${asyncPrefix}${f.name}, lines: ${f.lines[0]}-${f.lines[1]} }${comment}`;
7678
+ }
7679
+ function renderTypeDef(t) {
7680
+ const exportedSuffix = t.exported ? "" : " # local";
7681
+ return ` - { name: ${t.name}, lines: ${t.lines[0]}-${t.lines[1]} }${exportedSuffix}`;
7682
+ }
7683
+ function renderEnum(e) {
7684
+ const lines = [];
7685
+ const exportedSuffix = e.exported ? "" : " # local";
7686
+ lines.push(` - name: ${e.name}`);
7687
+ lines.push(` lines: ${e.lines[0]}-${e.lines[1]}${exportedSuffix}`);
7688
+ if (e.members.length > 0) {
7689
+ lines.push(` members: [${e.members.join(", ")}]`);
7690
+ }
7691
+ return lines;
7692
+ }
7693
+ function renderYaml(entries, opts) {
7694
+ const now = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "Z");
7695
+ const totalSrc = entries.reduce((s, e) => s + e.total_lines, 0);
7696
+ const bodyLines = [];
7697
+ for (let i = 0; i < entries.length; i++) {
7698
+ const e = entries[i];
7699
+ if (i > 0)
7700
+ bodyLines.push("");
7701
+ const pathKey = quotePath(e.path);
7702
+ bodyLines.push(`${pathKey}: # ${e.total_lines} lines`);
7703
+ if (e.imports.length > 0) {
7704
+ bodyLines.push(" imports:");
7705
+ for (const imp of e.imports) {
7706
+ const mod = quoteScalar(imp.module);
7707
+ bodyLines.push(` - { module: ${mod}, items: ${renderItems(imp.items)}, line: ${imp.line} }`);
7708
+ }
7709
+ }
7710
+ if (e.constants.length > 0) {
7711
+ bodyLines.push(" constants:");
7712
+ for (const c of e.constants) {
7713
+ bodyLines.push(` - { name: ${c.name}, line: ${c.line} }`);
7714
+ }
7715
+ }
7716
+ if (e.classes.length > 0) {
7717
+ bodyLines.push(" classes:");
7718
+ for (const c of e.classes) {
7719
+ bodyLines.push(...renderClass(c));
7720
+ }
7721
+ }
7722
+ if (e.functions.length > 0) {
7723
+ bodyLines.push(" functions:");
7724
+ for (const f of e.functions) {
7725
+ bodyLines.push(renderFunction(f));
7726
+ }
7727
+ }
7728
+ if (e.interfaces && e.interfaces.length > 0) {
7729
+ bodyLines.push(" interfaces:");
7730
+ for (const t of e.interfaces) {
7731
+ bodyLines.push(renderTypeDef(t));
7732
+ }
7733
+ }
7734
+ if (e.types && e.types.length > 0) {
7735
+ bodyLines.push(" types:");
7736
+ for (const t of e.types) {
7737
+ bodyLines.push(renderTypeDef(t));
7738
+ }
7739
+ }
7740
+ if (e.enums && e.enums.length > 0) {
7741
+ bodyLines.push(" enums:");
7742
+ for (const en of e.enums) {
7743
+ bodyLines.push(...renderEnum(en));
7744
+ }
7745
+ }
7746
+ }
7747
+ const headerLineCount = opts.sourcesDigest ? 3 : 2;
7748
+ const mapLines = bodyLines.length + headerLineCount;
7749
+ const compression = totalSrc === 0 ? 0 : totalSrc / mapLines;
7750
+ const fileCount = entries.length;
7751
+ const header = [
7752
+ `# generated: ${now} by ${opts.generator}`,
7753
+ `# files: ${fileCount}, src-lines: ${totalSrc}, map-lines: ${mapLines}, compression: ${compression.toFixed(1)}x`
7754
+ ];
7755
+ if (opts.sourcesDigest) {
7756
+ header.push(`# sources-digest: ${opts.sourcesDigest}`);
7757
+ }
7758
+ return [...header, ...bodyLines].join("\n") + "\n";
7759
+ }
7760
+ var YAML_RESERVED;
7761
+ var init_yaml_writer = __esm({
7762
+ "src/code-map/yaml-writer.ts"() {
7763
+ "use strict";
7764
+ YAML_RESERVED = /[:[\]{},&*!|>'"%@`#]/;
7765
+ }
7766
+ });
7767
+
7768
+ // src/code-map/orchestrator.ts
7769
+ async function scanInProcess(scanner, absolutePaths, repoRoot) {
7770
+ const entries = [];
7771
+ const warnings = [];
7772
+ for (const absPath of absolutePaths) {
7619
7773
  try {
7620
- const mtime = statSync3(absPath).mtimeMs;
7621
- if (mtime > mapMtime) {
7622
- const rel = absPath.replace(/\\/g, "/").replace(cwd.replace(/\\/g, "/") + "/", "");
7623
- staleAll.push(rel);
7774
+ const entry = await scanner.scan(absPath, repoRoot);
7775
+ if (entry === null) {
7776
+ const rel = absPath.replace(/\\/g, "/").replace(repoRoot.replace(/\\/g, "/") + "/", "");
7777
+ warnings.push({ path: rel, message: "parse error (scanner returned null)" });
7778
+ } else {
7779
+ entries.push(entry);
7624
7780
  }
7625
- } catch {
7781
+ } catch (err) {
7782
+ const rel = absPath.replace(/\\/g, "/").replace(repoRoot.replace(/\\/g, "/") + "/", "");
7783
+ warnings.push({ path: rel, message: `IO error: ${err.message}` });
7626
7784
  }
7627
7785
  }
7628
- if (staleAll.length === 0) {
7629
- return { status: "ok", staleFiles: [], staleCount: 0, mapPath };
7786
+ return { entries, warnings };
7787
+ }
7788
+ function bucketByExtension(files) {
7789
+ const out = {};
7790
+ for (const f of files) {
7791
+ const dot = f.lastIndexOf(".");
7792
+ const ext = dot >= 0 ? f.slice(dot).toLowerCase() : "";
7793
+ if (!out[ext])
7794
+ out[ext] = [];
7795
+ out[ext].push(f);
7630
7796
  }
7631
- return {
7632
- status: "stale",
7633
- staleFiles: staleAll.slice(0, 5),
7634
- staleCount: staleAll.length,
7635
- mapPath
7636
- };
7797
+ return out;
7637
7798
  }
7638
- var init_freshness = __esm({
7639
- "src/code-map/freshness.ts"() {
7799
+ var init_orchestrator = __esm({
7800
+ "src/code-map/orchestrator.ts"() {
7640
7801
  "use strict";
7641
7802
  init_include_exclude();
7642
- init_config();
7643
7803
  }
7644
7804
  });
7645
7805
 
7646
- // src/commands/migrate.ts
7647
- var migrate_exports = {};
7648
- __export(migrate_exports, {
7649
- migrate: () => migrate
7806
+ // src/code-map/scanners/common.ts
7807
+ import { relative as relative4 } from "path";
7808
+ function canonicalRelPath(absolutePath, repoRoot) {
7809
+ const rel = relative4(repoRoot, absolutePath);
7810
+ return rel.replace(/\\/g, "/").normalize("NFC");
7811
+ }
7812
+ function isAllCapsConst(name) {
7813
+ return name.length >= 2 && /^[A-Z][A-Z0-9_]*$/.test(name);
7814
+ }
7815
+ function isBinary(content) {
7816
+ const head = content.slice(0, 4096);
7817
+ return head.includes("\0");
7818
+ }
7819
+ var init_common = __esm({
7820
+ "src/code-map/scanners/common.ts"() {
7821
+ "use strict";
7822
+ }
7650
7823
  });
7651
- import { join as join16 } from "path";
7652
- 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";
7653
- import yaml3 from "js-yaml";
7654
- function backupChangeDir(cwd, changeId, sessionStamp) {
7655
- const backupRoot = join16(cwd, ".cdd", "migrate-backup", sessionStamp);
7656
- const backupDir2 = join16(backupRoot, changeId);
7657
- mkdirSync6(backupRoot, { recursive: true });
7658
- const sourceDir = join16(cwd, "specs", "changes", changeId);
7659
- if (existsSync14(sourceDir)) {
7660
- cpSync2(sourceDir, backupDir2, { recursive: true });
7824
+
7825
+ // src/code-map/scanners/python.ts
7826
+ var python_exports = {};
7827
+ __export(python_exports, {
7828
+ pythonScanner: () => pythonScanner
7829
+ });
7830
+ import { spawnSync as spawnSync2 } from "child_process";
7831
+ import { writeFileSync as writeFileSync5, unlinkSync } from "fs";
7832
+ import { join as join13 } from "path";
7833
+ import { tmpdir } from "os";
7834
+ function detectPython2() {
7835
+ for (const candidate of ["python3", "python"]) {
7836
+ try {
7837
+ const result = spawnSync2(candidate, ["--version"], {
7838
+ encoding: "utf8",
7839
+ timeout: 5e3
7840
+ });
7841
+ if (result.status === 0)
7842
+ return candidate;
7843
+ } catch {
7844
+ }
7661
7845
  }
7662
- return backupDir2;
7846
+ return null;
7663
7847
  }
7664
- function buildLegacyContextManifest(changeId) {
7665
- return [
7666
- "# Context Manifest",
7667
- "",
7668
- "Generated by `cdd-kit migrate` for an existing change.",
7669
- "Legacy manifest. Forbidden paths come from `.cdd/context-policy.json`.",
7670
- "",
7671
- "## Affected Surfaces",
7672
- "- legacy-unknown",
7673
- "",
7674
- "## Allowed Paths",
7675
- `- specs/changes/${changeId}/`,
7676
- "",
7677
- "## Required Contracts",
7678
- "- legacy-unknown",
7679
- "",
7680
- "## Required Tests",
7681
- "- legacy-unknown",
7682
- "",
7683
- "## Agent Work Packets",
7684
- "",
7685
- "## Context Expansion Requests",
7686
- "-",
7687
- "",
7688
- "## Approved Expansions",
7689
- "-",
7690
- ""
7691
- ].join("\n");
7692
- }
7693
- function buildContextGovernedManifest(changeId) {
7694
- return [
7695
- "# Context Manifest",
7696
- "",
7697
- "Generated by `cdd-kit migrate --enable-context-governance` for an existing change.",
7698
- "Review and narrow the allowed paths before assigning implementation work.",
7699
- "Forbidden paths come from `.cdd/context-policy.json`.",
7700
- "",
7701
- "## Affected Surfaces",
7702
- "- legacy-unknown",
7703
- "",
7704
- "## Allowed Paths",
7705
- `- specs/changes/${changeId}/`,
7706
- "- specs/context/project-map.md",
7707
- "- specs/context/contracts-index.md",
7708
- "",
7709
- "## Required Contracts",
7710
- "- legacy-unknown",
7711
- "",
7712
- "## Required Tests",
7713
- "- legacy-unknown",
7714
- "",
7715
- "## Agent Work Packets",
7716
- "",
7717
- "### change-classifier",
7718
- "- allowed:",
7719
- ` - specs/changes/${changeId}/`,
7720
- " - specs/context/project-map.md",
7721
- " - specs/context/contracts-index.md",
7722
- "",
7723
- "## Context Expansion Requests",
7724
- "",
7725
- "<!--",
7726
- "Agents must request context expansion instead of reading outside their work packet.",
7727
- "Use this format only for real requests:",
7728
- "",
7729
- "- request-id: CER-001",
7730
- " requested_paths:",
7731
- " - src/example.ts",
7732
- " reason: why this file is required",
7733
- " status: pending",
7734
- "-->",
7735
- "",
7736
- "## Approved Expansions",
7737
- "-",
7738
- ""
7739
- ].join("\n");
7740
- }
7741
- function parseLegacyFrontmatter(content) {
7742
- const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
7743
- if (!m)
7744
- return {};
7745
- const out = {};
7746
- for (const line of m[1].split(/\r?\n/)) {
7747
- const colon = line.indexOf(":");
7748
- if (colon === -1)
7749
- continue;
7750
- const key = line.slice(0, colon).trim();
7751
- if (!key)
7752
- continue;
7753
- out[key] = line.slice(colon + 1).trim();
7754
- }
7755
- return out;
7756
- }
7757
- function parseListField(raw) {
7758
- if (!raw)
7759
- return [];
7760
- const trimmed = raw.trim();
7761
- if (!trimmed || trimmed === "[]")
7762
- return [];
7763
- const inner = trimmed.startsWith("[") && trimmed.endsWith("]") ? trimmed.slice(1, -1) : trimmed;
7764
- return inner.split(",").map((item) => item.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
7765
- }
7766
- function parseLegacyTaskList(body) {
7767
- const lines = body.split(/\r?\n/);
7768
- const rows = [];
7769
- let currentSection;
7770
- for (const raw of lines) {
7771
- const headerMatch = raw.match(/^##\s+\d+\.\s+(.*)\s*$/);
7772
- if (headerMatch) {
7773
- currentSection = headerMatch[1].trim();
7774
- continue;
7775
- }
7776
- const itemMatch = raw.match(/^\s*-\s*\[([ xX\-])\]\s+(\d+(?:\.\d+)*)\s+(.*)\s*$/);
7777
- if (!itemMatch)
7778
- continue;
7779
- const mark = itemMatch[1];
7780
- const id = itemMatch[2];
7781
- const title = itemMatch[3].trim();
7782
- let status = "pending";
7783
- if (mark === "x" || mark === "X")
7784
- status = "done";
7785
- else if (mark === "-")
7786
- status = "skipped";
7787
- rows.push({ id, title, status, section: currentSection });
7788
- }
7789
- return rows;
7790
- }
7791
- function migrateTasksFile(changeId, changeDir, enableContextGovernance, detectedTier, changed, warnings, pendingWrites, pendingDeletes) {
7792
- const newPath = join16(changeDir, "tasks.yml");
7793
- const legacyPath = join16(changeDir, "tasks.md");
7794
- if (existsSync14(newPath)) {
7795
- return;
7796
- }
7797
- if (!existsSync14(legacyPath)) {
7798
- warnings.push("tasks.md not found and tasks.yml missing \u2014 skipping tasks migration");
7799
- return;
7800
- }
7801
- const raw = readFileSync11(legacyPath, "utf8");
7802
- const fm = parseLegacyFrontmatter(raw);
7803
- const bodyMatch = raw.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?([\s\S]*)$/);
7804
- const body = bodyMatch ? bodyMatch[1] : raw;
7805
- const tasksRows = parseLegacyTaskList(body);
7806
- const data = {};
7807
- data["change-id"] = fm["change-id"] || changeId;
7808
- data["status"] = fm["status"] || "in-progress";
7809
- if (fm["tier"] && /^\d+$/.test(fm["tier"])) {
7810
- data["tier"] = parseInt(fm["tier"], 10);
7811
- } else if (detectedTier) {
7812
- data["tier"] = parseInt(detectedTier, 10);
7813
- } else {
7814
- data["tier"] = null;
7815
- }
7816
- if (enableContextGovernance || fm["context-governance"] === "v1") {
7817
- data["context-governance"] = "v1";
7818
- }
7819
- const archive2 = parseListField(fm["archive-tasks"]);
7820
- data["archive-tasks"] = archive2.length > 0 ? archive2 : ["7.1", "7.2"];
7821
- const deps = parseListField(fm["depends-on"]);
7822
- data["depends-on"] = deps;
7823
- data["tasks"] = tasksRows.map((r) => {
7824
- const out = { id: r.id, title: r.title, status: r.status };
7825
- if (r.section)
7826
- out["section"] = r.section;
7827
- return out;
7828
- });
7829
- const yamlOut = yaml3.dump(data, { lineWidth: -1, noRefs: true });
7830
- pendingWrites.push({ path: newPath, content: yamlOut });
7831
- pendingDeletes.push({ path: legacyPath });
7832
- changed.push(`tasks.md -> tasks.yml (${tasksRows.length} task(s) migrated)`);
7833
- }
7834
- function parseLegacyAgentLog(content) {
7835
- const lines = content.split(/\r?\n/);
7836
- const data = {};
7837
- let i = 0;
7838
- const topFieldRe = /^[ ]{0,1}-\s*([\w-]+):\s*(.*)$/;
7839
- while (i < lines.length) {
7840
- const line = lines[i];
7841
- const fieldMatch = line.match(topFieldRe);
7842
- if (!fieldMatch) {
7843
- i++;
7844
- continue;
7845
- }
7846
- const key = fieldMatch[1];
7847
- const inline = fieldMatch[2].trim();
7848
- if (key === "files-read" || key === "artifacts") {
7849
- const items = [];
7850
- let j = i + 1;
7851
- while (j < lines.length) {
7852
- const sub = lines[j];
7853
- if (topFieldRe.test(sub))
7854
- break;
7855
- if (/^#/.test(sub))
7856
- break;
7857
- const itemMatch = sub.match(/^\s{2,}-\s+(.+?)\s*$/);
7858
- if (itemMatch) {
7859
- items.push(itemMatch[1].trim());
7848
+ var TIMEOUT_MS, PythonScanner, pythonScanner;
7849
+ var init_python = __esm({
7850
+ "src/code-map/scanners/python.ts"() {
7851
+ "use strict";
7852
+ init_paths();
7853
+ init_common();
7854
+ TIMEOUT_MS = parseInt(process.env["CDD_CODE_MAP_TIMEOUT_MS"] ?? "30000", 10);
7855
+ PythonScanner = class {
7856
+ extensions = [".py"];
7857
+ _interpreter = void 0;
7858
+ getInterpreter() {
7859
+ if (this._interpreter === void 0) {
7860
+ this._interpreter = detectPython2();
7860
7861
  }
7861
- j++;
7862
+ return this._interpreter;
7862
7863
  }
7863
- if (key === "files-read") {
7864
- data["files-read"] = items;
7865
- } else {
7866
- data["artifacts"] = items.map((s) => {
7867
- const idx = s.indexOf(":");
7868
- if (idx === -1) {
7869
- return { type: "note", pointer: s };
7864
+ async scan(absolutePath, repoRoot) {
7865
+ const result = await this.scanBatch([absolutePath], repoRoot);
7866
+ return result.entries[0] ?? null;
7867
+ }
7868
+ async scanBatch(absolutePaths, repoRoot) {
7869
+ const interpreter = this.getInterpreter();
7870
+ const entries = [];
7871
+ const warnings = [];
7872
+ if (!interpreter) {
7873
+ const count = absolutePaths.length;
7874
+ warnings.push({
7875
+ path: "",
7876
+ message: `python interpreter not found on PATH; skipping ${count} .py file${count === 1 ? "" : "s"}`
7877
+ });
7878
+ return { entries, warnings };
7879
+ }
7880
+ const scriptPath = ASSET.codeMapPython;
7881
+ const rand = Math.random().toString(36).slice(2);
7882
+ const listFile = join13(tmpdir(), `cdd-codemap-${process.pid}-${rand}.txt`);
7883
+ writeFileSync5(listFile, absolutePaths.join("\n") + "\n", "utf8");
7884
+ let stdout = "";
7885
+ let stderr = "";
7886
+ let exitCode = 0;
7887
+ try {
7888
+ const result = spawnSync2(
7889
+ interpreter,
7890
+ [scriptPath, "--batch-file", listFile, "--repo-root", repoRoot],
7891
+ {
7892
+ encoding: "utf8",
7893
+ timeout: TIMEOUT_MS,
7894
+ maxBuffer: 50 * 1024 * 1024
7895
+ // 50MB
7896
+ }
7897
+ );
7898
+ stdout = result.stdout ?? "";
7899
+ stderr = result.stderr ?? "";
7900
+ exitCode = result.status ?? -1;
7901
+ if (result.error) {
7902
+ const errMsg = result.error.message ?? String(result.error);
7903
+ if (errMsg.includes("ENOENT")) {
7904
+ warnings.push({
7905
+ path: "",
7906
+ message: `python interpreter not found (ENOENT); skipping ${absolutePaths.length} .py file(s)`
7907
+ });
7908
+ return { entries, warnings };
7909
+ }
7910
+ if (errMsg.includes("ETIMEDOUT") || errMsg.includes("timeout")) {
7911
+ warnings.push({
7912
+ path: "",
7913
+ message: `python scanner timed out after ${TIMEOUT_MS}ms; skipping .py files`
7914
+ });
7915
+ return { entries, warnings };
7916
+ }
7917
+ warnings.push({
7918
+ path: "",
7919
+ message: `python scanner error: ${errMsg}; skipping .py files`
7920
+ });
7921
+ return { entries, warnings };
7870
7922
  }
7871
- const type = s.slice(0, idx).trim();
7872
- const pointer = s.slice(idx + 1).trim();
7873
- return { type, pointer };
7874
- });
7923
+ } finally {
7924
+ try {
7925
+ unlinkSync(listFile);
7926
+ } catch {
7927
+ }
7928
+ }
7929
+ if (exitCode === 3) {
7930
+ warnings.push({
7931
+ path: "",
7932
+ message: "python interpreter is < 3.9 (need ast.unparse); skipping .py files"
7933
+ });
7934
+ return { entries, warnings };
7935
+ }
7936
+ for (const line of stdout.split("\n")) {
7937
+ const trimmed = line.trim();
7938
+ if (!trimmed)
7939
+ continue;
7940
+ let parsed;
7941
+ try {
7942
+ parsed = JSON.parse(trimmed);
7943
+ } catch {
7944
+ continue;
7945
+ }
7946
+ if (!parsed.ok) {
7947
+ warnings.push({ path: parsed.path, message: parsed.error });
7948
+ continue;
7949
+ }
7950
+ const r = parsed;
7951
+ entries.push({
7952
+ path: canonicalRelPath(join13(repoRoot, r.path), repoRoot),
7953
+ total_lines: r.total_lines,
7954
+ imports: r.imports ?? [],
7955
+ constants: r.constants ?? [],
7956
+ classes: (r.classes ?? []).map((c) => ({
7957
+ name: c.name,
7958
+ lines: [c.lines[0], c.lines[1]],
7959
+ methods: (c.methods ?? []).map((m) => ({
7960
+ name: m.name,
7961
+ lines: [m.lines[0], m.lines[1]],
7962
+ async: m.async
7963
+ }))
7964
+ })),
7965
+ functions: (r.functions ?? []).map((f) => ({
7966
+ name: f.name,
7967
+ lines: [f.lines[0], f.lines[1]],
7968
+ decorators: f.decorators ?? [],
7969
+ async: f.async
7970
+ }))
7971
+ });
7972
+ }
7973
+ if (exitCode === 2) {
7974
+ warnings.push({
7975
+ path: "",
7976
+ message: `python scanner exited with code 2 (fatal); partial results only. stderr: ${stderr.slice(0, 200)}`
7977
+ });
7978
+ }
7979
+ return { entries, warnings };
7875
7980
  }
7876
- i = j;
7877
- continue;
7878
- }
7879
- data[key] = inline;
7880
- i++;
7981
+ };
7982
+ pythonScanner = new PythonScanner();
7881
7983
  }
7882
- return data;
7984
+ });
7985
+
7986
+ // src/code-map/scanners/javascript.ts
7987
+ var javascript_exports = {};
7988
+ __export(javascript_exports, {
7989
+ COMMON_PLUGINS: () => COMMON_PLUGINS,
7990
+ countSourceLines: () => countSourceLines,
7991
+ jsScanner: () => jsScanner,
7992
+ parseAndExtract: () => parseAndExtract,
7993
+ parseJsSource: () => parseJsSource,
7994
+ parseSourceWithPlugins: () => parseSourceWithPlugins
7995
+ });
7996
+ import { readFileSync as readFileSync10 } from "fs";
7997
+ import { parse } from "@babel/parser";
7998
+ function parseSourceWithPlugins(source, plugins) {
7999
+ return parse(source, {
8000
+ sourceType: "unambiguous",
8001
+ allowReturnOutsideFunction: true,
8002
+ allowAwaitOutsideFunction: true,
8003
+ errorRecovery: true,
8004
+ plugins
8005
+ });
8006
+ }
8007
+ function isCallExpression(node, callee) {
8008
+ if (!node || node.type !== "CallExpression")
8009
+ return false;
8010
+ const c = node.callee;
8011
+ return c.type === "Identifier" && c.name === callee;
8012
+ }
8013
+ function extractRequireModule(node) {
8014
+ if (!node || node.type !== "CallExpression")
8015
+ return null;
8016
+ const c = node.callee;
8017
+ if (c.type !== "Identifier" || c.name !== "require")
8018
+ return null;
8019
+ const arg = node.arguments[0];
8020
+ if (!arg || arg.type !== "StringLiteral")
8021
+ return null;
8022
+ return arg.value;
7883
8023
  }
7884
- function migrateAgentLogs(changeDir, changed, pendingWrites, pendingDeletes) {
7885
- const agentLogDir = join16(changeDir, "agent-log");
7886
- if (!existsSync14(agentLogDir))
7887
- return;
7888
- const mdLogs = readdirSync8(agentLogDir).filter((f) => f.endsWith(".md"));
7889
- for (const f of mdLogs) {
7890
- const fullPath = join16(agentLogDir, f);
7891
- const yamlName = f.replace(/\.md$/, ".yml");
7892
- const yamlFull = join16(agentLogDir, yamlName);
7893
- if (existsSync14(yamlFull))
7894
- continue;
7895
- const raw = readFileSync11(fullPath, "utf8");
7896
- const parsed = parseLegacyAgentLog(raw);
7897
- const yamlOut = yaml3.dump(parsed, { lineWidth: -1, noRefs: true });
7898
- pendingWrites.push({ path: yamlFull, content: yamlOut });
7899
- pendingDeletes.push({ path: fullPath });
7900
- changed.push(`agent-log/${f} -> agent-log/${yamlName}`);
7901
- }
8024
+ function getLineRange(node) {
8025
+ const start = node.loc?.start.line ?? 1;
8026
+ const end = node.loc?.end.line ?? start;
8027
+ return [start, end];
7902
8028
  }
7903
- function migrateOne(changeId, changeDir, enableContextGovernance) {
7904
- const changed = [];
7905
- const warnings = [];
7906
- const pending = [];
7907
- const deletes = [];
7908
- let detectedTier = null;
7909
- const classifPath = join16(changeDir, "change-classification.md");
7910
- if (existsSync14(classifPath)) {
7911
- const content = readFileSync11(classifPath, "utf8");
7912
- const hasNewTierFormat = /^## Tier\s*\n\s*-\s*\d\s*$/m.test(content);
7913
- const oldMatch = content.match(/\*\*Tier[:\*]+\s*(?:Tier\s*)?(\d)/i) ?? content.match(/^-?\s*Tier:\s*(?:Tier\s*)?(\d)/mi);
7914
- if (oldMatch)
7915
- detectedTier = oldMatch[1];
7916
- if (!hasNewTierFormat) {
7917
- if (detectedTier) {
7918
- const addition = `
7919
- ## Tier
7920
- - ${detectedTier}
7921
- `;
7922
- if (!content.includes("\n## Tier\n")) {
7923
- changed.push(
7924
- `change-classification.md: appended "## Tier\\n- ${detectedTier}" (converted from old format)`
7925
- );
7926
- pending.push({ path: classifPath, content: content + addition });
7927
- }
7928
- } else {
7929
- warnings.push(
7930
- "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."
7931
- );
7932
- }
7933
- } else {
7934
- const structured = content.match(/^## Tier\s*\n\s*-\s*(\d)\s*$/m);
7935
- if (structured)
7936
- detectedTier = structured[1];
8029
+ function processImportDeclaration(node) {
8030
+ const items = [];
8031
+ for (const spec of node.specifiers) {
8032
+ if (spec.type === "ImportDefaultSpecifier") {
8033
+ items.push(`default:${spec.local.name}`);
8034
+ } else if (spec.type === "ImportNamespaceSpecifier") {
8035
+ items.push(`*:${spec.local.name}`);
8036
+ } else if (spec.type === "ImportSpecifier") {
8037
+ const imported = spec.imported;
8038
+ items.push(imported.type === "Identifier" ? imported.name : spec.local.name);
7937
8039
  }
7938
8040
  }
7939
- migrateTasksFile(changeId, changeDir, enableContextGovernance, detectedTier, changed, warnings, pending, deletes);
7940
- migrateAgentLogs(changeDir, changed, pending, deletes);
7941
- const manifestPath = join16(changeDir, "context-manifest.md");
7942
- if (!existsSync14(manifestPath)) {
7943
- changed.push(enableContextGovernance ? "context-manifest.md: added context-governance v1 manifest scaffold" : "context-manifest.md: added legacy context manifest scaffold");
7944
- pending.push({
7945
- path: manifestPath,
7946
- content: enableContextGovernance ? buildContextGovernedManifest(changeId) : buildLegacyContextManifest(changeId)
7947
- });
7948
- } else if (enableContextGovernance) {
7949
- warnings.push("context-manifest.md already exists \u2014 review allowed paths before relying on context-governance: v1");
7950
- }
7951
- return { result: { changed, warnings }, pending, deletes };
8041
+ return {
8042
+ module: node.source.value,
8043
+ items,
8044
+ line: node.loc?.start.line ?? 1
8045
+ };
7952
8046
  }
7953
- function commitWritesAtomically(pending, deletes) {
7954
- const renames = [];
7955
- try {
7956
- for (const write of pending) {
7957
- const tmp = `${write.path}.cdd-migrate.tmp`;
7958
- writeFileSync6(tmp, write.content, "utf8");
7959
- renames.push({ tmp, final: write.path });
7960
- }
7961
- } catch (err) {
7962
- for (const r of renames) {
7963
- try {
7964
- rmSync2(r.tmp, { force: true });
7965
- } catch {
8047
+ function processFunctionDeclaration(node, nameOverride) {
8048
+ const name = nameOverride ?? node.id?.name;
8049
+ if (!name)
8050
+ return null;
8051
+ return {
8052
+ name,
8053
+ lines: getLineRange(node),
8054
+ decorators: [],
8055
+ async: node.async
8056
+ };
8057
+ }
8058
+ function processClassDeclaration(node, nameOverride) {
8059
+ const name = nameOverride ?? node.id?.name;
8060
+ if (!name)
8061
+ return null;
8062
+ const methods = [];
8063
+ for (const member of node.body.body) {
8064
+ if (member.type === "ClassMethod" || member.type === "ClassPrivateMethod") {
8065
+ const m = member;
8066
+ let methodName;
8067
+ if (m.key.type === "Identifier") {
8068
+ methodName = m.key.name;
8069
+ } else if (m.key.type === "PrivateName") {
8070
+ methodName = `#${m.key.id.name}`;
8071
+ } else {
8072
+ methodName = "<computed>";
7966
8073
  }
7967
- }
7968
- throw err;
7969
- }
7970
- for (const r of renames) {
7971
- renameSync(r.tmp, r.final);
7972
- }
7973
- for (const d of deletes) {
7974
- try {
7975
- rmSync2(d.path, { force: true });
7976
- } catch {
8074
+ methods.push({
8075
+ name: methodName,
8076
+ lines: getLineRange(m),
8077
+ async: m.async
8078
+ });
7977
8079
  }
7978
8080
  }
8081
+ return {
8082
+ name,
8083
+ lines: getLineRange(node),
8084
+ methods
8085
+ };
7979
8086
  }
7980
- async function migrate(changeId, opts = {}) {
7981
- const cwd = process.cwd();
7982
- const dryRun = opts.dryRun ?? false;
7983
- const enableContextGovernance = opts.enableContextGovernance ?? false;
7984
- const noBackup = opts.noBackup ?? false;
7985
- const idsToMigrate = [];
7986
- if (opts.all) {
7987
- const changesDir = join16(cwd, "specs", "changes");
7988
- if (!existsSync14(changesDir)) {
7989
- log.info("No specs/changes/ directory found \u2014 nothing to migrate.");
7990
- return;
7991
- }
7992
- idsToMigrate.push(
7993
- ...readdirSync8(changesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name)
7994
- );
7995
- } else if (changeId) {
7996
- const specificDir = join16(cwd, "specs", "changes", changeId);
7997
- if (!existsSync14(specificDir)) {
7998
- log.error(`Change not found: specs/changes/${changeId}`);
7999
- process.exit(1);
8000
- }
8001
- idsToMigrate.push(changeId);
8002
- } else {
8003
- log.error("Usage: cdd-kit migrate <change-id> | cdd-kit migrate --all [--dry-run] [--no-backup]");
8004
- process.exit(1);
8005
- }
8006
- if (idsToMigrate.length === 0) {
8007
- log.info("No changes found to migrate.");
8008
- return;
8009
- }
8010
- if (dryRun) {
8011
- log.info("Dry run \u2014 no files will be written.");
8012
- log.blank();
8013
- }
8014
- const sessionStamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
8015
- let migratedCount = 0;
8016
- let upToDateCount = 0;
8017
- const backupRoot = join16(cwd, ".cdd", "migrate-backup", sessionStamp);
8018
- for (const id of idsToMigrate) {
8019
- const changeDir = join16(cwd, "specs", "changes", id);
8020
- if (!existsSync14(changeDir)) {
8021
- log.warn(` ${id}: directory not found \u2014 skipping`);
8022
- continue;
8023
- }
8024
- const { result, pending, deletes } = migrateOne(id, changeDir, enableContextGovernance);
8025
- const { changed, warnings } = result;
8026
- if (changed.length === 0) {
8027
- log.info(` ${id}: already up to date`);
8028
- upToDateCount++;
8029
- for (const w of warnings)
8030
- log.warn(` ${id}: ${w}`);
8087
+ function processVariableDeclaration(node, imports, constants, functions) {
8088
+ for (const decl of node.declarations) {
8089
+ if (!decl.id || decl.id.type !== "Identifier")
8031
8090
  continue;
8032
- }
8033
- if (!dryRun) {
8034
- try {
8035
- if (!noBackup)
8036
- backupChangeDir(cwd, id, sessionStamp);
8037
- commitWritesAtomically(pending, deletes);
8038
- } catch (err) {
8039
- log.error(` ${id}: migration failed \u2014 ${err.message}`);
8040
- if (!noBackup) {
8041
- log.error(` ${id}: restore from .cdd/migrate-backup/${sessionStamp}/${id}/`);
8042
- }
8043
- process.exit(1);
8091
+ const varName = decl.id.name;
8092
+ const init2 = decl.init;
8093
+ if (init2 && isCallExpression(init2, "require")) {
8094
+ const mod = extractRequireModule(init2);
8095
+ if (mod) {
8096
+ imports.push({ module: mod, items: [`default:${varName}`], line: node.loc?.start.line ?? 1 });
8044
8097
  }
8045
- }
8046
- log.ok(` ${id}: migrated`);
8047
- for (const c of changed)
8048
- log.info(` + ${c}`);
8049
- migratedCount++;
8050
- for (const w of warnings)
8051
- log.warn(` ${id}: ${w}`);
8052
- }
8053
- log.blank();
8054
- if (dryRun) {
8055
- log.info(`Dry run complete: ${migratedCount} change(s) would be updated, ${upToDateCount} already up to date.`);
8056
- } else {
8057
- log.ok(`Migration complete: ${migratedCount} updated, ${upToDateCount} already up to date.`);
8058
- if (migratedCount > 0 && !noBackup) {
8059
- log.info(`Backup: ${backupRoot}`);
8060
- log.info('Next: git add specs/changes/ && git commit -m "chore: migrate changes to YAML format"');
8061
- log.info("When stable, remove backup: rm -rf .cdd/migrate-backup/");
8062
- }
8063
- }
8064
- }
8065
- var init_migrate = __esm({
8066
- "src/commands/migrate.ts"() {
8067
- "use strict";
8068
- init_logger();
8069
- }
8070
- });
8071
-
8072
- // src/commands/upgrade.ts
8073
- var upgrade_exports = {};
8074
- __export(upgrade_exports, {
8075
- upgrade: () => upgrade
8076
- });
8077
- import { existsSync as existsSync15, mkdirSync as mkdirSync7, readdirSync as readdirSync9, copyFileSync as copyFileSync3, readFileSync as readFileSync12, writeFileSync as writeFileSync7 } from "fs";
8078
- import { dirname as dirname4, join as join17, relative as relative3 } from "path";
8079
- function planMissingFiles(srcDir, destDir, label, planned) {
8080
- if (!existsSync15(srcDir))
8081
- return;
8082
- for (const entry of readdirSync9(srcDir, { withFileTypes: true })) {
8083
- const src = join17(srcDir, entry.name);
8084
- const dest = join17(destDir, entry.name);
8085
- if (entry.isDirectory()) {
8086
- planMissingFiles(src, dest, join17(label, entry.name), planned);
8087
8098
  continue;
8088
8099
  }
8089
- if (!existsSync15(dest)) {
8090
- planned.push({ src, dest, rel: join17(label, relative3(srcDir, src)) });
8091
- }
8092
- }
8093
- }
8094
- function planProviderGuidance(cwd, provider, planned) {
8095
- if (provider === "claude" || provider === "both") {
8096
- if (!existsSync15(join17(cwd, "CLAUDE.md"))) {
8097
- planned.push({ src: ASSET.claudeTemplate, dest: join17(cwd, "CLAUDE.md"), rel: "CLAUDE.md" });
8100
+ if (isAllCapsConst(varName) && init2 !== null && init2 !== void 0) {
8101
+ constants.push({ name: varName, line: node.loc?.start.line ?? 1 });
8102
+ continue;
8098
8103
  }
8099
- if (!existsSync15(join17(cwd, "AGENTS.md"))) {
8100
- planned.push({ src: ASSET.agentsTemplate, dest: join17(cwd, "AGENTS.md"), rel: "AGENTS.md" });
8104
+ if (init2 && (init2.type === "ArrowFunctionExpression" || init2.type === "FunctionExpression")) {
8105
+ functions.push({
8106
+ name: varName,
8107
+ lines: getLineRange(node),
8108
+ decorators: [],
8109
+ async: init2.async
8110
+ });
8111
+ } else if (init2 && init2.type === "CallExpression" && /^[A-Z]/.test(varName)) {
8112
+ functions.push({
8113
+ name: varName,
8114
+ lines: getLineRange(node),
8115
+ decorators: [],
8116
+ async: false
8117
+ });
8101
8118
  }
8102
8119
  }
8103
- if ((provider === "codex" || provider === "both") && !existsSync15(join17(cwd, "CODEX.md"))) {
8104
- planned.push({ src: ASSET.codexTemplate, dest: join17(cwd, "CODEX.md"), rel: "CODEX.md" });
8105
- }
8106
8120
  }
8107
- function applyCopy(plan) {
8108
- for (const item of plan) {
8109
- mkdirSync7(dirname4(item.dest), { recursive: true });
8110
- copyFileSync3(item.src, item.dest);
8111
- }
8121
+ function processTsInterfaceDeclaration(node, exported) {
8122
+ if (!node || node.type !== "TSInterfaceDeclaration")
8123
+ return null;
8124
+ if (!node.id || node.id.type !== "Identifier")
8125
+ return null;
8126
+ return {
8127
+ name: node.id.name,
8128
+ lines: getLineRange(node),
8129
+ exported
8130
+ };
8112
8131
  }
8113
- async function upgrade(opts = {}) {
8114
- const cwd = process.cwd();
8115
- const requestedProvider = opts.provider ?? "auto";
8116
- if (!validateProviderOption(requestedProvider)) {
8117
- log.error(`Invalid provider: ${requestedProvider}. Use auto, claude, codex, or both.`);
8118
- process.exit(1);
8132
+ function processTsTypeAliasDeclaration(node, exported) {
8133
+ if (!node || node.type !== "TSTypeAliasDeclaration")
8134
+ return null;
8135
+ if (!node.id || node.id.type !== "Identifier")
8136
+ return null;
8137
+ return {
8138
+ name: node.id.name,
8139
+ lines: getLineRange(node),
8140
+ exported
8141
+ };
8142
+ }
8143
+ function processTsEnumDeclaration(node, exported) {
8144
+ if (!node || node.type !== "TSEnumDeclaration")
8145
+ return null;
8146
+ if (!node.id || node.id.type !== "Identifier")
8147
+ return null;
8148
+ const members = [];
8149
+ for (const m of node.members ?? []) {
8150
+ if (!m || !m.id)
8151
+ continue;
8152
+ if (m.id.type === "Identifier")
8153
+ members.push(m.id.name);
8154
+ else if (m.id.type === "StringLiteral")
8155
+ members.push(m.id.value);
8119
8156
  }
8120
- const provider = inferProvider(cwd, requestedProvider);
8121
- const plan = [];
8122
- planMissingFiles(ASSET.contracts, join17(cwd, "contracts"), "contracts", plan);
8123
- planMissingFiles(ASSET.specsTemplates, join17(cwd, "specs", "templates"), "specs/templates", plan);
8124
- planMissingFiles(ASSET.testsTemplates, join17(cwd, "tests", "templates"), "tests/templates", plan);
8125
- planMissingFiles(ASSET.ci, join17(cwd, "ci"), "ci", plan);
8126
- planMissingFiles(ASSET.githubWorkflows, join17(cwd, ".github", "workflows"), ".github/workflows", plan);
8127
- planMissingFiles(ASSET.cddConfig, join17(cwd, ".cdd"), ".cdd", plan);
8128
- planProviderGuidance(cwd, provider, plan);
8129
- log.blank();
8130
- log.info(`Upgrade provider: ${provider}`);
8131
- if (plan.length === 0) {
8132
- log.ok("No missing cdd-kit project files found.");
8133
- if (opts.migrateChanges) {
8134
- log.blank();
8135
- log.info("Running change migration flow...");
8136
- await migrate(void 0, {
8137
- all: true,
8138
- dryRun: !opts.yes,
8139
- enableContextGovernance: opts.enableContextGovernance
8140
- });
8141
- }
8142
- log.blank();
8157
+ return {
8158
+ name: node.id.name,
8159
+ lines: getLineRange(node),
8160
+ exported,
8161
+ members
8162
+ };
8163
+ }
8164
+ function processStatement(stmt, buckets, extractTsTypes, exportedFromWrapper = false) {
8165
+ if (stmt.type === "ExportNamedDeclaration" && stmt.declaration) {
8166
+ processStatement(stmt.declaration, buckets, extractTsTypes, true);
8143
8167
  return;
8144
8168
  }
8145
- log.info(`${plan.length} missing file(s) detected:`);
8146
- for (const item of plan)
8147
- log.dim(` + ${item.rel.replace(/\\/g, "/")}`);
8148
- if (!opts.yes) {
8149
- log.blank();
8150
- log.info("Dry run only. Re-run with --yes to write missing files.");
8151
- if (opts.migrateChanges) {
8152
- log.blank();
8153
- log.info("Previewing existing change migration because --migrate-changes was requested.");
8154
- await migrate(void 0, {
8155
- all: true,
8156
- dryRun: true,
8157
- enableContextGovernance: opts.enableContextGovernance
8158
- });
8169
+ if (stmt.type === "ExportDefaultDeclaration") {
8170
+ const decl = stmt.declaration;
8171
+ if (decl.type === "FunctionDeclaration") {
8172
+ const fe = processFunctionDeclaration(decl, decl.id?.name ?? "default");
8173
+ if (fe)
8174
+ buckets.functions.push(fe);
8175
+ } else if (decl.type === "ClassDeclaration") {
8176
+ const ce = processClassDeclaration(decl, decl.id?.name ?? "default");
8177
+ if (ce)
8178
+ buckets.classes.push(ce);
8159
8179
  }
8160
- log.blank();
8161
8180
  return;
8162
8181
  }
8163
- applyCopy(plan);
8164
- const modelPolicyPath = join17(cwd, ".cdd", "model-policy.json");
8165
- if (existsSync15(modelPolicyPath)) {
8166
- let existing = {};
8167
- try {
8168
- existing = JSON.parse(readFileSync12(modelPolicyPath, "utf8"));
8169
- } catch {
8170
- }
8171
- const merged = {
8172
- ...existing,
8173
- provider,
8174
- generated_at: (/* @__PURE__ */ new Date()).toISOString(),
8175
- roles: existing.roles && typeof existing.roles === "object" ? existing.roles : {}
8176
- };
8177
- writeFileSync7(modelPolicyPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
8178
- }
8179
- log.blank();
8180
- log.ok(`Upgrade complete: ${plan.length} missing file(s) added.`);
8181
- log.info("Existing project guidance and contracts were preserved.");
8182
- if (opts.migrateChanges) {
8183
- log.blank();
8184
- log.info("Running change migration flow...");
8185
- await migrate(void 0, {
8186
- all: true,
8187
- dryRun: false,
8188
- enableContextGovernance: opts.enableContextGovernance
8189
- });
8190
- }
8191
- log.blank();
8192
- }
8193
- var init_upgrade = __esm({
8194
- "src/commands/upgrade.ts"() {
8195
- "use strict";
8196
- init_paths();
8197
- init_logger();
8198
- init_provider();
8199
- init_migrate();
8200
- }
8201
- });
8202
-
8203
- // src/code-map/yaml-writer.ts
8204
- function quoteScalar(s) {
8205
- if (s.startsWith(".") || YAML_RESERVED.test(s) || s.includes(" ") || s === "" || s === "null" || s === "true" || s === "false") {
8206
- return `'${s.replace(/'/g, "''")}'`;
8207
- }
8208
- return s;
8209
- }
8210
- function quotePath(p) {
8211
- if (YAML_RESERVED.test(p) || p.includes(" ")) {
8212
- return `'${p.replace(/'/g, "''")}'`;
8182
+ if (stmt.type === "ImportDeclaration") {
8183
+ buckets.imports.push(processImportDeclaration(stmt));
8184
+ return;
8213
8185
  }
8214
- return p;
8215
- }
8216
- function renderItems(items) {
8217
- if (items.length === 0)
8218
- return "[]";
8219
- return `[${items.map(quoteScalar).join(", ")}]`;
8220
- }
8221
- function truncateDecorator(d, max = 80) {
8222
- const cleaned = d.replace(/\r?\n/g, " ");
8223
- return cleaned.length <= max ? cleaned : cleaned.slice(0, max) + "...";
8224
- }
8225
- function renderClass(c) {
8226
- const lines = [];
8227
- lines.push(` - name: ${c.name}`);
8228
- lines.push(` lines: ${c.lines[0]}-${c.lines[1]}`);
8229
- if (c.methods.length > 0) {
8230
- lines.push(" methods:");
8231
- for (const m of c.methods) {
8232
- const asyncPrefix = m.async ? "async " : "";
8233
- lines.push(` - { name: ${asyncPrefix}${m.name}, lines: ${m.lines[0]}-${m.lines[1]} }`);
8234
- }
8186
+ if (stmt.type === "FunctionDeclaration") {
8187
+ const fe = processFunctionDeclaration(stmt);
8188
+ if (fe)
8189
+ buckets.functions.push(fe);
8190
+ return;
8235
8191
  }
8236
- return lines;
8237
- }
8238
- function renderFunction(f) {
8239
- const asyncPrefix = f.async ? "async " : "";
8240
- let comment = "";
8241
- if (f.decorators.length > 0) {
8242
- const truncated = truncateDecorator(f.decorators[0]);
8243
- comment = ` # @${truncated}`;
8192
+ if (stmt.type === "ClassDeclaration") {
8193
+ const ce = processClassDeclaration(stmt);
8194
+ if (ce)
8195
+ buckets.classes.push(ce);
8196
+ return;
8244
8197
  }
8245
- return ` - { name: ${asyncPrefix}${f.name}, lines: ${f.lines[0]}-${f.lines[1]} }${comment}`;
8246
- }
8247
- function renderTypeDef(t) {
8248
- const exportedSuffix = t.exported ? "" : " # local";
8249
- return ` - { name: ${t.name}, lines: ${t.lines[0]}-${t.lines[1]} }${exportedSuffix}`;
8250
- }
8251
- function renderEnum(e) {
8252
- const lines = [];
8253
- const exportedSuffix = e.exported ? "" : " # local";
8254
- lines.push(` - name: ${e.name}`);
8255
- lines.push(` lines: ${e.lines[0]}-${e.lines[1]}${exportedSuffix}`);
8256
- if (e.members.length > 0) {
8257
- lines.push(` members: [${e.members.join(", ")}]`);
8198
+ if (stmt.type === "VariableDeclaration") {
8199
+ processVariableDeclaration(stmt, buckets.imports, buckets.constants, buckets.functions);
8200
+ return;
8258
8201
  }
8259
- return lines;
8260
- }
8261
- function renderYaml(entries, opts) {
8262
- const now = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "Z");
8263
- const totalSrc = entries.reduce((s, e) => s + e.total_lines, 0);
8264
- const bodyLines = [];
8265
- for (let i = 0; i < entries.length; i++) {
8266
- const e = entries[i];
8267
- if (i > 0)
8268
- bodyLines.push("");
8269
- const pathKey = quotePath(e.path);
8270
- bodyLines.push(`${pathKey}: # ${e.total_lines} lines`);
8271
- if (e.imports.length > 0) {
8272
- bodyLines.push(" imports:");
8273
- for (const imp of e.imports) {
8274
- const mod = quoteScalar(imp.module);
8275
- bodyLines.push(` - { module: ${mod}, items: ${renderItems(imp.items)}, line: ${imp.line} }`);
8276
- }
8277
- }
8278
- if (e.constants.length > 0) {
8279
- bodyLines.push(" constants:");
8280
- for (const c of e.constants) {
8281
- bodyLines.push(` - { name: ${c.name}, line: ${c.line} }`);
8282
- }
8283
- }
8284
- if (e.classes.length > 0) {
8285
- bodyLines.push(" classes:");
8286
- for (const c of e.classes) {
8287
- bodyLines.push(...renderClass(c));
8288
- }
8289
- }
8290
- if (e.functions.length > 0) {
8291
- bodyLines.push(" functions:");
8292
- for (const f of e.functions) {
8293
- bodyLines.push(renderFunction(f));
8294
- }
8295
- }
8296
- if (e.interfaces && e.interfaces.length > 0) {
8297
- bodyLines.push(" interfaces:");
8298
- for (const t of e.interfaces) {
8299
- bodyLines.push(renderTypeDef(t));
8300
- }
8202
+ if (extractTsTypes) {
8203
+ const anyStmt = stmt;
8204
+ if (anyStmt.type === "TSInterfaceDeclaration") {
8205
+ const e = processTsInterfaceDeclaration(anyStmt, exportedFromWrapper);
8206
+ if (e)
8207
+ buckets.interfaces.push(e);
8208
+ return;
8301
8209
  }
8302
- if (e.types && e.types.length > 0) {
8303
- bodyLines.push(" types:");
8304
- for (const t of e.types) {
8305
- bodyLines.push(renderTypeDef(t));
8306
- }
8210
+ if (anyStmt.type === "TSTypeAliasDeclaration") {
8211
+ const e = processTsTypeAliasDeclaration(anyStmt, exportedFromWrapper);
8212
+ if (e)
8213
+ buckets.types.push(e);
8214
+ return;
8307
8215
  }
8308
- if (e.enums && e.enums.length > 0) {
8309
- bodyLines.push(" enums:");
8310
- for (const en of e.enums) {
8311
- bodyLines.push(...renderEnum(en));
8312
- }
8216
+ if (anyStmt.type === "TSEnumDeclaration") {
8217
+ const e = processTsEnumDeclaration(anyStmt, exportedFromWrapper);
8218
+ if (e)
8219
+ buckets.enums.push(e);
8220
+ return;
8313
8221
  }
8314
8222
  }
8315
- const mapLines = bodyLines.length + 2;
8316
- const compression = totalSrc === 0 ? 0 : totalSrc / mapLines;
8317
- const fileCount = entries.length;
8318
- const header = [
8319
- `# generated: ${now} by ${opts.generator}`,
8320
- `# files: ${fileCount}, src-lines: ${totalSrc}, map-lines: ${mapLines}, compression: ${compression.toFixed(1)}x`
8321
- ];
8322
- return [...header, ...bodyLines].join("\n") + "\n";
8323
- }
8324
- var YAML_RESERVED;
8325
- var init_yaml_writer = __esm({
8326
- "src/code-map/yaml-writer.ts"() {
8327
- "use strict";
8328
- YAML_RESERVED = /[:[\]{},&*!|>'"%@`#]/;
8329
- }
8330
- });
8331
-
8332
- // src/code-map/orchestrator.ts
8333
- async function scanInProcess(scanner, absolutePaths, repoRoot) {
8334
- const entries = [];
8335
- const warnings = [];
8336
- for (const absPath of absolutePaths) {
8337
- try {
8338
- const entry = await scanner.scan(absPath, repoRoot);
8339
- if (entry === null) {
8340
- const rel = absPath.replace(/\\/g, "/").replace(repoRoot.replace(/\\/g, "/") + "/", "");
8341
- warnings.push({ path: rel, message: "parse error (scanner returned null)" });
8342
- } else {
8343
- entries.push(entry);
8223
+ if (stmt.type === "ExpressionStatement") {
8224
+ const expr = stmt.expression;
8225
+ if (isCallExpression(expr, "require")) {
8226
+ const mod = extractRequireModule(expr);
8227
+ if (mod) {
8228
+ buckets.imports.push({ module: mod, items: [], line: stmt.loc?.start.line ?? 1 });
8344
8229
  }
8345
- } catch (err) {
8346
- const rel = absPath.replace(/\\/g, "/").replace(repoRoot.replace(/\\/g, "/") + "/", "");
8347
- warnings.push({ path: rel, message: `IO error: ${err.message}` });
8348
8230
  }
8349
8231
  }
8350
- return { entries, warnings };
8351
8232
  }
8352
- function bucketByExtension(files) {
8353
- const out = {};
8354
- for (const f of files) {
8355
- const dot = f.lastIndexOf(".");
8356
- const ext = dot >= 0 ? f.slice(dot).toLowerCase() : "";
8357
- if (!out[ext])
8358
- out[ext] = [];
8359
- out[ext].push(f);
8233
+ function parseAndExtract(source, opts) {
8234
+ let ast;
8235
+ try {
8236
+ ast = parseSourceWithPlugins(source, opts.plugins);
8237
+ } catch {
8238
+ return null;
8360
8239
  }
8361
- return out;
8362
- }
8363
- var init_orchestrator = __esm({
8364
- "src/code-map/orchestrator.ts"() {
8365
- "use strict";
8366
- init_include_exclude();
8240
+ const buckets = {
8241
+ imports: [],
8242
+ constants: [],
8243
+ functions: [],
8244
+ classes: [],
8245
+ interfaces: [],
8246
+ types: [],
8247
+ enums: []
8248
+ };
8249
+ for (const stmt of ast.program.body) {
8250
+ processStatement(stmt, buckets, !!opts.extractTsTypes);
8367
8251
  }
8368
- });
8369
-
8370
- // src/code-map/scanners/common.ts
8371
- import { relative as relative4 } from "path";
8372
- function canonicalRelPath(absolutePath, repoRoot) {
8373
- const rel = relative4(repoRoot, absolutePath);
8374
- return rel.replace(/\\/g, "/").normalize("NFC");
8252
+ return buckets;
8375
8253
  }
8376
- function isAllCapsConst(name) {
8377
- return name.length >= 2 && /^[A-Z][A-Z0-9_]*$/.test(name);
8254
+ function countSourceLines(source) {
8255
+ if (source === "")
8256
+ return 0;
8257
+ return source.split(/\r?\n/).length - (source.endsWith("\n") || source.endsWith("\r\n") ? 1 : 0);
8378
8258
  }
8379
- function isBinary(content) {
8380
- const head = content.slice(0, 4096);
8381
- return head.includes("\0");
8259
+ function parseJsSource(source, relPath) {
8260
+ const total_lines = countSourceLines(source);
8261
+ const r = parseAndExtract(source, { plugins: JS_PLUGINS, extractTsTypes: false });
8262
+ if (!r)
8263
+ return null;
8264
+ return {
8265
+ path: relPath,
8266
+ total_lines,
8267
+ imports: r.imports,
8268
+ constants: r.constants,
8269
+ classes: r.classes,
8270
+ functions: r.functions
8271
+ };
8382
8272
  }
8383
- var init_common = __esm({
8384
- "src/code-map/scanners/common.ts"() {
8273
+ var COMMON_PLUGINS, JS_PLUGINS, JavaScriptScanner, jsScanner;
8274
+ var init_javascript = __esm({
8275
+ "src/code-map/scanners/javascript.ts"() {
8385
8276
  "use strict";
8277
+ init_common();
8278
+ COMMON_PLUGINS = [
8279
+ "classProperties",
8280
+ "classPrivateProperties",
8281
+ "classPrivateMethods",
8282
+ "decorators-legacy",
8283
+ "topLevelAwait",
8284
+ "dynamicImport",
8285
+ "optionalChaining",
8286
+ "nullishCoalescingOperator",
8287
+ "objectRestSpread",
8288
+ "asyncGenerators",
8289
+ "numericSeparator",
8290
+ "logicalAssignment"
8291
+ ];
8292
+ JS_PLUGINS = ["jsx", ...COMMON_PLUGINS];
8293
+ JavaScriptScanner = class {
8294
+ extensions = [".js"];
8295
+ async scan(absolutePath, repoRoot) {
8296
+ let source;
8297
+ try {
8298
+ source = readFileSync10(absolutePath, "utf8");
8299
+ } catch (err) {
8300
+ throw err;
8301
+ }
8302
+ if (isBinary(source)) {
8303
+ return null;
8304
+ }
8305
+ const relPath = canonicalRelPath(absolutePath, repoRoot);
8306
+ return parseJsSource(source, relPath);
8307
+ }
8308
+ };
8309
+ jsScanner = new JavaScriptScanner();
8386
8310
  }
8387
8311
  });
8388
8312
 
8389
- // src/code-map/scanners/python.ts
8390
- var python_exports = {};
8391
- __export(python_exports, {
8392
- pythonScanner: () => pythonScanner
8313
+ // src/code-map/scanners/typescript.ts
8314
+ var typescript_exports = {};
8315
+ __export(typescript_exports, {
8316
+ tsScanner: () => tsScanner
8393
8317
  });
8394
- import { spawnSync as spawnSync2 } from "child_process";
8395
- import { writeFileSync as writeFileSync8, unlinkSync } from "fs";
8396
- import { join as join18 } from "path";
8397
- import { tmpdir } from "os";
8398
- function detectPython2() {
8399
- for (const candidate of ["python3", "python"]) {
8400
- try {
8401
- const result = spawnSync2(candidate, ["--version"], {
8402
- encoding: "utf8",
8403
- timeout: 5e3
8404
- });
8405
- if (result.status === 0)
8406
- return candidate;
8407
- } catch {
8408
- }
8409
- }
8410
- return null;
8411
- }
8412
- var TIMEOUT_MS, PythonScanner, pythonScanner;
8413
- var init_python = __esm({
8414
- "src/code-map/scanners/python.ts"() {
8318
+ import { readFileSync as readFileSync11 } from "fs";
8319
+ var TypeScriptScanner, tsScanner;
8320
+ var init_typescript = __esm({
8321
+ "src/code-map/scanners/typescript.ts"() {
8415
8322
  "use strict";
8416
- init_paths();
8417
8323
  init_common();
8418
- TIMEOUT_MS = parseInt(process.env["CDD_CODE_MAP_TIMEOUT_MS"] ?? "30000", 10);
8419
- PythonScanner = class {
8420
- extensions = [".py"];
8421
- _interpreter = void 0;
8422
- getInterpreter() {
8423
- if (this._interpreter === void 0) {
8424
- this._interpreter = detectPython2();
8425
- }
8426
- return this._interpreter;
8427
- }
8324
+ init_javascript();
8325
+ TypeScriptScanner = class {
8326
+ extensions = [".ts", ".tsx"];
8428
8327
  async scan(absolutePath, repoRoot) {
8429
- const result = await this.scanBatch([absolutePath], repoRoot);
8430
- return result.entries[0] ?? null;
8431
- }
8432
- async scanBatch(absolutePaths, repoRoot) {
8433
- const interpreter = this.getInterpreter();
8434
- const entries = [];
8435
- const warnings = [];
8436
- if (!interpreter) {
8437
- const count = absolutePaths.length;
8438
- warnings.push({
8439
- path: "",
8440
- message: `python interpreter not found on PATH; skipping ${count} .py file${count === 1 ? "" : "s"}`
8441
- });
8442
- return { entries, warnings };
8328
+ let source;
8329
+ try {
8330
+ source = readFileSync11(absolutePath, "utf8");
8331
+ } catch (err) {
8332
+ throw err;
8443
8333
  }
8444
- const scriptPath = ASSET.codeMapPython;
8445
- const rand = Math.random().toString(36).slice(2);
8446
- const listFile = join18(tmpdir(), `cdd-codemap-${process.pid}-${rand}.txt`);
8447
- writeFileSync8(listFile, absolutePaths.join("\n") + "\n", "utf8");
8448
- let stdout = "";
8449
- let stderr = "";
8450
- let exitCode = 0;
8334
+ if (isBinary(source)) {
8335
+ return null;
8336
+ }
8337
+ const isTsx = absolutePath.toLowerCase().endsWith(".tsx");
8338
+ const plugins = isTsx ? ["typescript", "jsx", ...COMMON_PLUGINS] : ["typescript", ...COMMON_PLUGINS];
8339
+ const r = parseAndExtract(source, { plugins, extractTsTypes: true });
8340
+ if (!r)
8341
+ return null;
8342
+ const relPath = canonicalRelPath(absolutePath, repoRoot);
8343
+ const total_lines = countSourceLines(source);
8344
+ return {
8345
+ path: relPath,
8346
+ total_lines,
8347
+ imports: r.imports,
8348
+ constants: r.constants,
8349
+ classes: r.classes,
8350
+ functions: r.functions,
8351
+ interfaces: r.interfaces,
8352
+ types: r.types,
8353
+ enums: r.enums
8354
+ };
8355
+ }
8356
+ };
8357
+ tsScanner = new TypeScriptScanner();
8358
+ }
8359
+ });
8360
+
8361
+ // src/code-map/scanners/vue.ts
8362
+ var vue_exports = {};
8363
+ __export(vue_exports, {
8364
+ vueScanner: () => vueScanner
8365
+ });
8366
+ import { readFileSync as readFileSync12 } from "fs";
8367
+ import { parse as parse2 } from "@vue/compiler-sfc";
8368
+ var VueScanner, vueScanner;
8369
+ var init_vue = __esm({
8370
+ "src/code-map/scanners/vue.ts"() {
8371
+ "use strict";
8372
+ init_common();
8373
+ init_javascript();
8374
+ VueScanner = class {
8375
+ extensions = [".vue"];
8376
+ async scan(absolutePath, repoRoot) {
8377
+ let source;
8451
8378
  try {
8452
- const result = spawnSync2(
8453
- interpreter,
8454
- [scriptPath, "--batch-file", listFile, "--repo-root", repoRoot],
8455
- {
8456
- encoding: "utf8",
8457
- timeout: TIMEOUT_MS,
8458
- maxBuffer: 50 * 1024 * 1024
8459
- // 50MB
8460
- }
8461
- );
8462
- stdout = result.stdout ?? "";
8463
- stderr = result.stderr ?? "";
8464
- exitCode = result.status ?? -1;
8465
- if (result.error) {
8466
- const errMsg = result.error.message ?? String(result.error);
8467
- if (errMsg.includes("ENOENT")) {
8468
- warnings.push({
8469
- path: "",
8470
- message: `python interpreter not found (ENOENT); skipping ${absolutePaths.length} .py file(s)`
8471
- });
8472
- return { entries, warnings };
8473
- }
8474
- if (errMsg.includes("ETIMEDOUT") || errMsg.includes("timeout")) {
8475
- warnings.push({
8476
- path: "",
8477
- message: `python scanner timed out after ${TIMEOUT_MS}ms; skipping .py files`
8478
- });
8479
- return { entries, warnings };
8480
- }
8481
- warnings.push({
8482
- path: "",
8483
- message: `python scanner error: ${errMsg}; skipping .py files`
8484
- });
8485
- return { entries, warnings };
8486
- }
8487
- } finally {
8488
- try {
8489
- unlinkSync(listFile);
8490
- } catch {
8491
- }
8379
+ source = readFileSync12(absolutePath, "utf8");
8380
+ } catch (err) {
8381
+ throw err;
8492
8382
  }
8493
- if (exitCode === 3) {
8494
- warnings.push({
8495
- path: "",
8496
- message: "python interpreter is < 3.9 (need ast.unparse); skipping .py files"
8497
- });
8498
- return { entries, warnings };
8383
+ if (isBinary(source)) {
8384
+ return null;
8499
8385
  }
8500
- for (const line of stdout.split("\n")) {
8501
- const trimmed = line.trim();
8502
- if (!trimmed)
8386
+ const relPath = canonicalRelPath(absolutePath, repoRoot);
8387
+ const total_lines = source.split(/\r?\n/).length - (source.endsWith("\n") || source.endsWith("\r\n") ? 1 : 0);
8388
+ const { descriptor } = parse2(source, { filename: absolutePath });
8389
+ const allImports = [];
8390
+ const allConstants = [];
8391
+ const allFunctions = [];
8392
+ const allClasses = [];
8393
+ const warnings = [];
8394
+ const scriptBlocks = [descriptor.script, descriptor.scriptSetup].filter(Boolean);
8395
+ if (scriptBlocks.length === 0) {
8396
+ return {
8397
+ path: relPath,
8398
+ total_lines,
8399
+ imports: [],
8400
+ constants: [],
8401
+ classes: [],
8402
+ functions: []
8403
+ };
8404
+ }
8405
+ for (const block of scriptBlocks) {
8406
+ if (!block)
8503
8407
  continue;
8504
- let parsed;
8505
- try {
8506
- parsed = JSON.parse(trimmed);
8507
- } catch {
8408
+ if (block.lang === "ts" || block.lang === "tsx") {
8409
+ warnings.push({
8410
+ path: relPath,
8411
+ message: `skipping <script lang=${block.lang}> block (TypeScript not supported in v1)`
8412
+ });
8508
8413
  continue;
8509
8414
  }
8510
- if (!parsed.ok) {
8511
- warnings.push({ path: parsed.path, message: parsed.error });
8415
+ const blockContent = block.content;
8416
+ const lineOffset = block.loc.start.line;
8417
+ const scriptEntry = parseJsSource(blockContent, relPath);
8418
+ if (!scriptEntry) {
8419
+ warnings.push({ path: relPath, message: "parse error in script block" });
8512
8420
  continue;
8513
8421
  }
8514
- const r = parsed;
8515
- entries.push({
8516
- path: canonicalRelPath(join18(repoRoot, r.path), repoRoot),
8517
- total_lines: r.total_lines,
8518
- imports: r.imports ?? [],
8519
- constants: r.constants ?? [],
8520
- classes: (r.classes ?? []).map((c) => ({
8521
- name: c.name,
8522
- lines: [c.lines[0], c.lines[1]],
8523
- methods: (c.methods ?? []).map((m) => ({
8524
- name: m.name,
8525
- lines: [m.lines[0], m.lines[1]],
8526
- async: m.async
8422
+ const offset = lineOffset - 1;
8423
+ for (const imp of scriptEntry.imports) {
8424
+ allImports.push({ ...imp, line: imp.line + offset });
8425
+ }
8426
+ for (const c of scriptEntry.constants) {
8427
+ allConstants.push({ ...c, line: c.line + offset });
8428
+ }
8429
+ for (const fn of scriptEntry.functions) {
8430
+ allFunctions.push({
8431
+ ...fn,
8432
+ lines: [fn.lines[0] + offset, fn.lines[1] + offset]
8433
+ });
8434
+ }
8435
+ for (const cls of scriptEntry.classes) {
8436
+ allClasses.push({
8437
+ ...cls,
8438
+ lines: [cls.lines[0] + offset, cls.lines[1] + offset],
8439
+ methods: cls.methods.map((m) => ({
8440
+ ...m,
8441
+ lines: [m.lines[0] + offset, m.lines[1] + offset]
8527
8442
  }))
8528
- })),
8529
- functions: (r.functions ?? []).map((f) => ({
8530
- name: f.name,
8531
- lines: [f.lines[0], f.lines[1]],
8532
- decorators: f.decorators ?? [],
8533
- async: f.async
8534
- }))
8535
- });
8536
- }
8537
- if (exitCode === 2) {
8538
- warnings.push({
8539
- path: "",
8540
- message: `python scanner exited with code 2 (fatal); partial results only. stderr: ${stderr.slice(0, 200)}`
8541
- });
8443
+ });
8444
+ }
8542
8445
  }
8543
- return { entries, warnings };
8446
+ return {
8447
+ path: relPath,
8448
+ total_lines,
8449
+ imports: allImports,
8450
+ constants: allConstants,
8451
+ classes: allClasses,
8452
+ functions: allFunctions
8453
+ };
8544
8454
  }
8545
8455
  };
8546
- pythonScanner = new PythonScanner();
8456
+ vueScanner = new VueScanner();
8547
8457
  }
8548
8458
  });
8549
8459
 
8550
- // src/code-map/scanners/javascript.ts
8551
- var javascript_exports = {};
8552
- __export(javascript_exports, {
8553
- COMMON_PLUGINS: () => COMMON_PLUGINS,
8554
- countSourceLines: () => countSourceLines,
8555
- jsScanner: () => jsScanner,
8556
- parseAndExtract: () => parseAndExtract,
8557
- parseJsSource: () => parseJsSource,
8558
- parseSourceWithPlugins: () => parseSourceWithPlugins
8460
+ // src/commands/code-map.ts
8461
+ var code_map_exports = {};
8462
+ __export(code_map_exports, {
8463
+ codeMap: () => codeMap,
8464
+ computeSourcesDigest: () => computeSourcesDigest
8559
8465
  });
8560
- import { readFileSync as readFileSync14 } from "fs";
8561
- import { parse } from "@babel/parser";
8562
- function parseSourceWithPlugins(source, plugins) {
8563
- return parse(source, {
8564
- sourceType: "unambiguous",
8565
- allowReturnOutsideFunction: true,
8566
- allowAwaitOutsideFunction: true,
8567
- errorRecovery: true,
8568
- plugins
8466
+ import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync13, writeFileSync as writeFileSync6 } from "fs";
8467
+ import { resolve, dirname as dirname4, relative as relative5 } from "path";
8468
+ import { createHash as createHash4 } from "crypto";
8469
+ import { createRequire } from "module";
8470
+ import { fileURLToPath as fileURLToPath2 } from "url";
8471
+ import { join as join14 } from "path";
8472
+ function computeSourcesDigest(absolutePaths, cwd) {
8473
+ const lines = absolutePaths.slice().sort().map((p) => {
8474
+ const rel = relative5(cwd, p).replace(/\\/g, "/");
8475
+ let contentHash;
8476
+ try {
8477
+ contentHash = createHash4("sha256").update(readFileSync13(p)).digest("hex");
8478
+ } catch {
8479
+ contentHash = "missing";
8480
+ }
8481
+ return `${rel}:${contentHash}`;
8569
8482
  });
8483
+ return createHash4("sha256").update(lines.join("\n")).digest("hex");
8570
8484
  }
8571
- function isCallExpression(node, callee) {
8572
- if (!node || node.type !== "CallExpression")
8573
- return false;
8574
- const c = node.callee;
8575
- return c.type === "Identifier" && c.name === callee;
8576
- }
8577
- function extractRequireModule(node) {
8578
- if (!node || node.type !== "CallExpression")
8579
- return null;
8580
- const c = node.callee;
8581
- if (c.type !== "Identifier" || c.name !== "require")
8582
- return null;
8583
- const arg = node.arguments[0];
8584
- if (!arg || arg.type !== "StringLiteral")
8585
- return null;
8586
- return arg.value;
8587
- }
8588
- function getLineRange(node) {
8589
- const start = node.loc?.start.line ?? 1;
8590
- const end = node.loc?.end.line ?? start;
8591
- return [start, end];
8592
- }
8593
- function processImportDeclaration(node) {
8594
- const items = [];
8595
- for (const spec of node.specifiers) {
8596
- if (spec.type === "ImportDefaultSpecifier") {
8597
- items.push(`default:${spec.local.name}`);
8598
- } else if (spec.type === "ImportNamespaceSpecifier") {
8599
- items.push(`*:${spec.local.name}`);
8600
- } else if (spec.type === "ImportSpecifier") {
8601
- const imported = spec.imported;
8602
- items.push(imported.type === "Identifier" ? imported.name : spec.local.name);
8485
+ async function codeMap(opts) {
8486
+ const root = resolve(process.cwd(), opts.path);
8487
+ const start = Date.now();
8488
+ let cfg;
8489
+ try {
8490
+ cfg = loadCodeMapConfig(root);
8491
+ } catch (err) {
8492
+ log.error(`code-map: ${err.message}`);
8493
+ return 1;
8494
+ }
8495
+ const include = [...cfg.include, ...opts.include];
8496
+ const exclude = [...cfg.exclude, ...opts.exclude];
8497
+ const files = walkRepo(root, { include, exclude });
8498
+ const buckets = bucketByExtension(files);
8499
+ const result = { entries: [], warnings: [] };
8500
+ const tasks = [];
8501
+ if (buckets[".py"]?.length) {
8502
+ const { pythonScanner: pythonScanner2 } = await Promise.resolve().then(() => (init_python(), python_exports));
8503
+ if (pythonScanner2.scanBatch) {
8504
+ tasks.push(pythonScanner2.scanBatch(buckets[".py"], root));
8603
8505
  }
8604
8506
  }
8605
- return {
8606
- module: node.source.value,
8607
- items,
8608
- line: node.loc?.start.line ?? 1
8609
- };
8610
- }
8611
- function processFunctionDeclaration(node, nameOverride) {
8612
- const name = nameOverride ?? node.id?.name;
8613
- if (!name)
8614
- return null;
8615
- return {
8616
- name,
8617
- lines: getLineRange(node),
8618
- decorators: [],
8619
- async: node.async
8620
- };
8621
- }
8622
- function processClassDeclaration(node, nameOverride) {
8623
- const name = nameOverride ?? node.id?.name;
8624
- if (!name)
8625
- return null;
8626
- const methods = [];
8627
- for (const member of node.body.body) {
8628
- if (member.type === "ClassMethod" || member.type === "ClassPrivateMethod") {
8629
- const m = member;
8630
- let methodName;
8631
- if (m.key.type === "Identifier") {
8632
- methodName = m.key.name;
8633
- } else if (m.key.type === "PrivateName") {
8634
- methodName = `#${m.key.id.name}`;
8635
- } else {
8636
- methodName = "<computed>";
8637
- }
8638
- methods.push({
8639
- name: methodName,
8640
- lines: getLineRange(m),
8641
- async: m.async
8507
+ const jsFiles = [
8508
+ ...buckets[".js"] ?? [],
8509
+ ...buckets[".jsx"] ?? [],
8510
+ ...buckets[".mjs"] ?? [],
8511
+ ...buckets[".cjs"] ?? []
8512
+ ];
8513
+ if (jsFiles.length) {
8514
+ const { jsScanner: jsScanner2 } = await Promise.resolve().then(() => (init_javascript(), javascript_exports));
8515
+ tasks.push(scanInProcess(jsScanner2, jsFiles, root));
8516
+ }
8517
+ const tsFiles = [
8518
+ ...buckets[".ts"] ?? [],
8519
+ ...buckets[".tsx"] ?? []
8520
+ ];
8521
+ if (tsFiles.length) {
8522
+ const { tsScanner: tsScanner2 } = await Promise.resolve().then(() => (init_typescript(), typescript_exports));
8523
+ tasks.push(scanInProcess(tsScanner2, tsFiles, root));
8524
+ }
8525
+ if (buckets[".vue"]?.length) {
8526
+ const { vueScanner: vueScanner2 } = await Promise.resolve().then(() => (init_vue(), vue_exports));
8527
+ tasks.push(scanInProcess(vueScanner2, buckets[".vue"], root));
8528
+ }
8529
+ for (const r of await Promise.all(tasks)) {
8530
+ result.entries.push(...r.entries);
8531
+ result.warnings.push(...r.warnings);
8532
+ }
8533
+ result.entries.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
8534
+ for (const e of result.entries) {
8535
+ if (e.total_lines > opts.maxLines) {
8536
+ result.warnings.push({
8537
+ path: e.path,
8538
+ message: `file exceeds --max-lines (${e.total_lines} > ${opts.maxLines})`
8642
8539
  });
8643
8540
  }
8644
8541
  }
8645
- return {
8646
- name,
8647
- lines: getLineRange(node),
8648
- methods
8649
- };
8542
+ const sourcesDigest = computeSourcesDigest(files, root);
8543
+ const yamlBody = renderYaml(result.entries, {
8544
+ generator: `cdd-kit ${_pkg.version}`,
8545
+ sourcesDigest
8546
+ });
8547
+ const totalSrc = result.entries.reduce((s, e) => s + e.total_lines, 0);
8548
+ const mapLines = yamlBody.split("\n").length;
8549
+ const compression = totalSrc === 0 ? 0 : totalSrc / mapLines;
8550
+ const summaryLine = `scanned ${result.entries.length} files, ${totalSrc} src lines -> ${opts.out} (${mapLines} lines, compression ${compression.toFixed(1)}x)`;
8551
+ for (const w of result.warnings) {
8552
+ log.warn(`${w.path}: ${w.message}`);
8553
+ }
8554
+ if (opts.check) {
8555
+ const existing = existsSync11(opts.out) ? readFileSync13(opts.out, "utf8") : "";
8556
+ const normalize = (s) => s.replace(/^# generated: [^\n]+\n/m, "# generated: <normalized>\n");
8557
+ if (normalize(existing) !== normalize(yamlBody)) {
8558
+ log.error(`code-map out of date: ${opts.out} would change. Run \`cdd-kit code-map\` to regenerate.`);
8559
+ return 1;
8560
+ }
8561
+ log.ok(`code-map up to date: ${opts.out}`);
8562
+ return 0;
8563
+ }
8564
+ mkdirSync5(dirname4(opts.out), { recursive: true });
8565
+ writeFileSync6(opts.out, yamlBody, "utf8");
8566
+ log.ok(`${summaryLine} (${Date.now() - start}ms)`);
8567
+ return 0;
8650
8568
  }
8651
- function processVariableDeclaration(node, imports, constants, functions) {
8652
- for (const decl of node.declarations) {
8653
- if (!decl.id || decl.id.type !== "Identifier")
8654
- continue;
8655
- const varName = decl.id.name;
8656
- const init2 = decl.init;
8657
- if (init2 && isCallExpression(init2, "require")) {
8658
- const mod = extractRequireModule(init2);
8659
- if (mod) {
8660
- imports.push({ module: mod, items: [`default:${varName}`], line: node.loc?.start.line ?? 1 });
8661
- }
8662
- continue;
8569
+ var _require, _pkgPath, _pkg;
8570
+ var init_code_map = __esm({
8571
+ "src/commands/code-map.ts"() {
8572
+ "use strict";
8573
+ init_logger();
8574
+ init_yaml_writer();
8575
+ init_orchestrator();
8576
+ init_config();
8577
+ _require = createRequire(import.meta.url);
8578
+ _pkgPath = join14(fileURLToPath2(import.meta.url), "..", "..", "..", "package.json");
8579
+ _pkg = JSON.parse(readFileSync13(_pkgPath, "utf8"));
8580
+ }
8581
+ });
8582
+
8583
+ // src/code-map/freshness.ts
8584
+ var freshness_exports = {};
8585
+ __export(freshness_exports, {
8586
+ checkCodeMapFreshness: () => checkCodeMapFreshness
8587
+ });
8588
+ import { existsSync as existsSync12, readFileSync as readFileSync14, statSync as statSync3 } from "fs";
8589
+ import { join as join15 } from "path";
8590
+ function checkCodeMapFreshness(cwd, mapRel = ".cdd/code-map.yml", include, exclude) {
8591
+ const mapPath = join15(cwd, mapRel);
8592
+ let cfg;
8593
+ try {
8594
+ cfg = loadCodeMapConfig(cwd);
8595
+ } catch (err) {
8596
+ return {
8597
+ status: "config-error",
8598
+ staleFiles: [],
8599
+ staleCount: 0,
8600
+ mapPath,
8601
+ configError: err.message
8602
+ };
8603
+ }
8604
+ const includeFinal = [...cfg.include, ...include ?? []];
8605
+ const excludeFinal = [...cfg.exclude, ...exclude ?? []];
8606
+ const sourceFiles = walkRepo(cwd, { include: includeFinal, exclude: excludeFinal });
8607
+ if (!existsSync12(mapPath)) {
8608
+ if (sourceFiles.length === 0) {
8609
+ return { status: "missing-greenfield", staleFiles: [], staleCount: 0, mapPath };
8663
8610
  }
8664
- if (isAllCapsConst(varName) && init2 !== null && init2 !== void 0) {
8665
- constants.push({ name: varName, line: node.loc?.start.line ?? 1 });
8666
- continue;
8611
+ return { status: "missing-with-sources", staleFiles: [], staleCount: 0, mapPath };
8612
+ }
8613
+ const mapMtime = statSync3(mapPath).mtimeMs;
8614
+ const staleAll = [];
8615
+ for (const absPath of sourceFiles) {
8616
+ try {
8617
+ const mtime = statSync3(absPath).mtimeMs;
8618
+ if (mtime > mapMtime) {
8619
+ const rel = absPath.replace(/\\/g, "/").replace(cwd.replace(/\\/g, "/") + "/", "");
8620
+ staleAll.push(rel);
8621
+ }
8622
+ } catch {
8667
8623
  }
8668
- if (init2 && (init2.type === "ArrowFunctionExpression" || init2.type === "FunctionExpression")) {
8669
- functions.push({
8670
- name: varName,
8671
- lines: getLineRange(node),
8672
- decorators: [],
8673
- async: init2.async
8674
- });
8675
- } else if (init2 && init2.type === "CallExpression" && /^[A-Z]/.test(varName)) {
8676
- functions.push({
8677
- name: varName,
8678
- lines: getLineRange(node),
8679
- decorators: [],
8680
- async: false
8681
- });
8624
+ }
8625
+ if (staleAll.length === 0) {
8626
+ return { status: "ok", staleFiles: [], staleCount: 0, mapPath };
8627
+ }
8628
+ const declaredDigest = readSourcesDigest(mapPath);
8629
+ if (declaredDigest !== null) {
8630
+ const actualDigest = computeSourcesDigest(sourceFiles, cwd);
8631
+ if (actualDigest === declaredDigest) {
8632
+ return { status: "ok", staleFiles: [], staleCount: 0, mapPath };
8682
8633
  }
8683
8634
  }
8684
- }
8685
- function processTsInterfaceDeclaration(node, exported) {
8686
- if (!node || node.type !== "TSInterfaceDeclaration")
8687
- return null;
8688
- if (!node.id || node.id.type !== "Identifier")
8689
- return null;
8690
8635
  return {
8691
- name: node.id.name,
8692
- lines: getLineRange(node),
8693
- exported
8636
+ status: "stale",
8637
+ staleFiles: staleAll.slice(0, 5),
8638
+ staleCount: staleAll.length,
8639
+ mapPath
8694
8640
  };
8695
8641
  }
8696
- function processTsTypeAliasDeclaration(node, exported) {
8697
- if (!node || node.type !== "TSTypeAliasDeclaration")
8698
- return null;
8699
- if (!node.id || node.id.type !== "Identifier")
8642
+ function readSourcesDigest(mapPath) {
8643
+ try {
8644
+ const head = readFileSync14(mapPath, "utf8").slice(0, 2048);
8645
+ const m = head.match(/^# sources-digest:\s*([a-f0-9]+)/m);
8646
+ return m ? m[1] : null;
8647
+ } catch {
8700
8648
  return null;
8701
- return {
8702
- name: node.id.name,
8703
- lines: getLineRange(node),
8704
- exported
8705
- };
8649
+ }
8650
+ }
8651
+ var init_freshness = __esm({
8652
+ "src/code-map/freshness.ts"() {
8653
+ "use strict";
8654
+ init_include_exclude();
8655
+ init_config();
8656
+ init_code_map();
8657
+ }
8658
+ });
8659
+
8660
+ // src/commands/migrate.ts
8661
+ var migrate_exports = {};
8662
+ __export(migrate_exports, {
8663
+ migrate: () => migrate
8664
+ });
8665
+ import { join as join18 } from "path";
8666
+ import { cpSync as cpSync2, existsSync as existsSync15, mkdirSync as mkdirSync7, readdirSync as readdirSync8, readFileSync as readFileSync17, renameSync, rmSync as rmSync2, writeFileSync as writeFileSync8 } from "fs";
8667
+ import yaml3 from "js-yaml";
8668
+ function backupChangeDir(cwd, changeId, sessionStamp) {
8669
+ const backupRoot = join18(cwd, ".cdd", "migrate-backup", sessionStamp);
8670
+ const backupDir2 = join18(backupRoot, changeId);
8671
+ mkdirSync7(backupRoot, { recursive: true });
8672
+ const sourceDir = join18(cwd, "specs", "changes", changeId);
8673
+ if (existsSync15(sourceDir)) {
8674
+ cpSync2(sourceDir, backupDir2, { recursive: true });
8675
+ }
8676
+ return backupDir2;
8677
+ }
8678
+ function buildLegacyContextManifest(changeId) {
8679
+ return [
8680
+ "# Context Manifest",
8681
+ "",
8682
+ "Generated by `cdd-kit migrate` for an existing change.",
8683
+ "Legacy manifest. Forbidden paths come from `.cdd/context-policy.json`.",
8684
+ "",
8685
+ "## Affected Surfaces",
8686
+ "- legacy-unknown",
8687
+ "",
8688
+ "## Allowed Paths",
8689
+ `- specs/changes/${changeId}/`,
8690
+ "",
8691
+ "## Required Contracts",
8692
+ "- legacy-unknown",
8693
+ "",
8694
+ "## Required Tests",
8695
+ "- legacy-unknown",
8696
+ "",
8697
+ "## Agent Work Packets",
8698
+ "",
8699
+ "## Context Expansion Requests",
8700
+ "-",
8701
+ "",
8702
+ "## Approved Expansions",
8703
+ "-",
8704
+ ""
8705
+ ].join("\n");
8706
+ }
8707
+ function buildContextGovernedManifest(changeId) {
8708
+ return [
8709
+ "# Context Manifest",
8710
+ "",
8711
+ "Generated by `cdd-kit migrate --enable-context-governance` for an existing change.",
8712
+ "Review and narrow the allowed paths before assigning implementation work.",
8713
+ "Forbidden paths come from `.cdd/context-policy.json`.",
8714
+ "",
8715
+ "## Affected Surfaces",
8716
+ "- legacy-unknown",
8717
+ "",
8718
+ "## Allowed Paths",
8719
+ `- specs/changes/${changeId}/`,
8720
+ "- specs/context/project-map.md",
8721
+ "- specs/context/contracts-index.md",
8722
+ "",
8723
+ "## Required Contracts",
8724
+ "- legacy-unknown",
8725
+ "",
8726
+ "## Required Tests",
8727
+ "- legacy-unknown",
8728
+ "",
8729
+ "## Agent Work Packets",
8730
+ "",
8731
+ "### change-classifier",
8732
+ "- allowed:",
8733
+ ` - specs/changes/${changeId}/`,
8734
+ " - specs/context/project-map.md",
8735
+ " - specs/context/contracts-index.md",
8736
+ "",
8737
+ "## Context Expansion Requests",
8738
+ "",
8739
+ "<!--",
8740
+ "Agents must request context expansion instead of reading outside their work packet.",
8741
+ "Use this format only for real requests:",
8742
+ "",
8743
+ "- request-id: CER-001",
8744
+ " requested_paths:",
8745
+ " - src/example.ts",
8746
+ " reason: why this file is required",
8747
+ " status: pending",
8748
+ "-->",
8749
+ "",
8750
+ "## Approved Expansions",
8751
+ "-",
8752
+ ""
8753
+ ].join("\n");
8706
8754
  }
8707
- function processTsEnumDeclaration(node, exported) {
8708
- if (!node || node.type !== "TSEnumDeclaration")
8709
- return null;
8710
- if (!node.id || node.id.type !== "Identifier")
8711
- return null;
8712
- const members = [];
8713
- for (const m of node.members ?? []) {
8714
- if (!m || !m.id)
8755
+ function parseLegacyFrontmatter(content) {
8756
+ const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
8757
+ if (!m)
8758
+ return {};
8759
+ const out = {};
8760
+ for (const line of m[1].split(/\r?\n/)) {
8761
+ const colon = line.indexOf(":");
8762
+ if (colon === -1)
8715
8763
  continue;
8716
- if (m.id.type === "Identifier")
8717
- members.push(m.id.name);
8718
- else if (m.id.type === "StringLiteral")
8719
- members.push(m.id.value);
8764
+ const key = line.slice(0, colon).trim();
8765
+ if (!key)
8766
+ continue;
8767
+ out[key] = line.slice(colon + 1).trim();
8720
8768
  }
8721
- return {
8722
- name: node.id.name,
8723
- lines: getLineRange(node),
8724
- exported,
8725
- members
8726
- };
8769
+ return out;
8727
8770
  }
8728
- function processStatement(stmt, buckets, extractTsTypes, exportedFromWrapper = false) {
8729
- if (stmt.type === "ExportNamedDeclaration" && stmt.declaration) {
8730
- processStatement(stmt.declaration, buckets, extractTsTypes, true);
8731
- return;
8732
- }
8733
- if (stmt.type === "ExportDefaultDeclaration") {
8734
- const decl = stmt.declaration;
8735
- if (decl.type === "FunctionDeclaration") {
8736
- const fe = processFunctionDeclaration(decl, decl.id?.name ?? "default");
8737
- if (fe)
8738
- buckets.functions.push(fe);
8739
- } else if (decl.type === "ClassDeclaration") {
8740
- const ce = processClassDeclaration(decl, decl.id?.name ?? "default");
8741
- if (ce)
8742
- buckets.classes.push(ce);
8771
+ function parseListField(raw) {
8772
+ if (!raw)
8773
+ return [];
8774
+ const trimmed = raw.trim();
8775
+ if (!trimmed || trimmed === "[]")
8776
+ return [];
8777
+ const inner = trimmed.startsWith("[") && trimmed.endsWith("]") ? trimmed.slice(1, -1) : trimmed;
8778
+ return inner.split(",").map((item) => item.trim().replace(/^["']|["']$/g, "")).filter(Boolean);
8779
+ }
8780
+ function parseLegacyTaskList(body) {
8781
+ const lines = body.split(/\r?\n/);
8782
+ const rows = [];
8783
+ let currentSection;
8784
+ for (const raw of lines) {
8785
+ const headerMatch = raw.match(/^##\s+\d+\.\s+(.*)\s*$/);
8786
+ if (headerMatch) {
8787
+ currentSection = headerMatch[1].trim();
8788
+ continue;
8743
8789
  }
8744
- return;
8790
+ const itemMatch = raw.match(/^\s*-\s*\[([ xX\-])\]\s+(\d+(?:\.\d+)*)\s+(.*)\s*$/);
8791
+ if (!itemMatch)
8792
+ continue;
8793
+ const mark = itemMatch[1];
8794
+ const id = itemMatch[2];
8795
+ const title = itemMatch[3].trim();
8796
+ let status = "pending";
8797
+ if (mark === "x" || mark === "X")
8798
+ status = "done";
8799
+ else if (mark === "-")
8800
+ status = "skipped";
8801
+ rows.push({ id, title, status, section: currentSection });
8745
8802
  }
8746
- if (stmt.type === "ImportDeclaration") {
8747
- buckets.imports.push(processImportDeclaration(stmt));
8803
+ return rows;
8804
+ }
8805
+ function migrateTasksFile(changeId, changeDir, enableContextGovernance, detectedTier, changed, warnings, pendingWrites, pendingDeletes) {
8806
+ const newPath = join18(changeDir, "tasks.yml");
8807
+ const legacyPath = join18(changeDir, "tasks.md");
8808
+ if (existsSync15(newPath)) {
8748
8809
  return;
8749
8810
  }
8750
- if (stmt.type === "FunctionDeclaration") {
8751
- const fe = processFunctionDeclaration(stmt);
8752
- if (fe)
8753
- buckets.functions.push(fe);
8811
+ if (!existsSync15(legacyPath)) {
8812
+ warnings.push("tasks.md not found and tasks.yml missing \u2014 skipping tasks migration");
8754
8813
  return;
8755
8814
  }
8756
- if (stmt.type === "ClassDeclaration") {
8757
- const ce = processClassDeclaration(stmt);
8758
- if (ce)
8759
- buckets.classes.push(ce);
8760
- return;
8815
+ const raw = readFileSync17(legacyPath, "utf8");
8816
+ const fm = parseLegacyFrontmatter(raw);
8817
+ const bodyMatch = raw.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n?([\s\S]*)$/);
8818
+ const body = bodyMatch ? bodyMatch[1] : raw;
8819
+ const tasksRows = parseLegacyTaskList(body);
8820
+ const data = {};
8821
+ data["change-id"] = fm["change-id"] || changeId;
8822
+ data["status"] = fm["status"] || "in-progress";
8823
+ if (fm["tier"] && /^\d+$/.test(fm["tier"])) {
8824
+ data["tier"] = parseInt(fm["tier"], 10);
8825
+ } else if (detectedTier) {
8826
+ data["tier"] = parseInt(detectedTier, 10);
8827
+ } else {
8828
+ data["tier"] = null;
8761
8829
  }
8762
- if (stmt.type === "VariableDeclaration") {
8763
- processVariableDeclaration(stmt, buckets.imports, buckets.constants, buckets.functions);
8764
- return;
8830
+ if (enableContextGovernance || fm["context-governance"] === "v1") {
8831
+ data["context-governance"] = "v1";
8765
8832
  }
8766
- if (extractTsTypes) {
8767
- const anyStmt = stmt;
8768
- if (anyStmt.type === "TSInterfaceDeclaration") {
8769
- const e = processTsInterfaceDeclaration(anyStmt, exportedFromWrapper);
8770
- if (e)
8771
- buckets.interfaces.push(e);
8772
- return;
8773
- }
8774
- if (anyStmt.type === "TSTypeAliasDeclaration") {
8775
- const e = processTsTypeAliasDeclaration(anyStmt, exportedFromWrapper);
8776
- if (e)
8777
- buckets.types.push(e);
8778
- return;
8833
+ const archive2 = parseListField(fm["archive-tasks"]);
8834
+ data["archive-tasks"] = archive2.length > 0 ? archive2 : ["7.1", "7.2"];
8835
+ const deps = parseListField(fm["depends-on"]);
8836
+ data["depends-on"] = deps;
8837
+ data["tasks"] = tasksRows.map((r) => {
8838
+ const out = { id: r.id, title: r.title, status: r.status };
8839
+ if (r.section)
8840
+ out["section"] = r.section;
8841
+ return out;
8842
+ });
8843
+ const yamlOut = yaml3.dump(data, { lineWidth: -1, noRefs: true });
8844
+ pendingWrites.push({ path: newPath, content: yamlOut });
8845
+ pendingDeletes.push({ path: legacyPath });
8846
+ changed.push(`tasks.md -> tasks.yml (${tasksRows.length} task(s) migrated)`);
8847
+ }
8848
+ function parseLegacyAgentLog(content) {
8849
+ const lines = content.split(/\r?\n/);
8850
+ const data = {};
8851
+ let i = 0;
8852
+ const topFieldRe = /^[ ]{0,1}-\s*([\w-]+):\s*(.*)$/;
8853
+ while (i < lines.length) {
8854
+ const line = lines[i];
8855
+ const fieldMatch = line.match(topFieldRe);
8856
+ if (!fieldMatch) {
8857
+ i++;
8858
+ continue;
8779
8859
  }
8780
- if (anyStmt.type === "TSEnumDeclaration") {
8781
- const e = processTsEnumDeclaration(anyStmt, exportedFromWrapper);
8782
- if (e)
8783
- buckets.enums.push(e);
8784
- return;
8860
+ const key = fieldMatch[1];
8861
+ const inline = fieldMatch[2].trim();
8862
+ if (key === "files-read" || key === "artifacts") {
8863
+ const items = [];
8864
+ let j = i + 1;
8865
+ while (j < lines.length) {
8866
+ const sub = lines[j];
8867
+ if (topFieldRe.test(sub))
8868
+ break;
8869
+ if (/^#/.test(sub))
8870
+ break;
8871
+ const itemMatch = sub.match(/^\s{2,}-\s+(.+?)\s*$/);
8872
+ if (itemMatch) {
8873
+ items.push(itemMatch[1].trim());
8874
+ }
8875
+ j++;
8876
+ }
8877
+ if (key === "files-read") {
8878
+ data["files-read"] = items;
8879
+ } else {
8880
+ data["artifacts"] = items.map((s) => {
8881
+ const idx = s.indexOf(":");
8882
+ if (idx === -1) {
8883
+ return { type: "note", pointer: s };
8884
+ }
8885
+ const type = s.slice(0, idx).trim();
8886
+ const pointer = s.slice(idx + 1).trim();
8887
+ return { type, pointer };
8888
+ });
8889
+ }
8890
+ i = j;
8891
+ continue;
8785
8892
  }
8893
+ data[key] = inline;
8894
+ i++;
8786
8895
  }
8787
- if (stmt.type === "ExpressionStatement") {
8788
- const expr = stmt.expression;
8789
- if (isCallExpression(expr, "require")) {
8790
- const mod = extractRequireModule(expr);
8791
- if (mod) {
8792
- buckets.imports.push({ module: mod, items: [], line: stmt.loc?.start.line ?? 1 });
8896
+ return data;
8897
+ }
8898
+ function migrateAgentLogs(changeDir, changed, pendingWrites, pendingDeletes) {
8899
+ const agentLogDir = join18(changeDir, "agent-log");
8900
+ if (!existsSync15(agentLogDir))
8901
+ return;
8902
+ const mdLogs = readdirSync8(agentLogDir).filter((f) => f.endsWith(".md"));
8903
+ for (const f of mdLogs) {
8904
+ const fullPath = join18(agentLogDir, f);
8905
+ const yamlName = f.replace(/\.md$/, ".yml");
8906
+ const yamlFull = join18(agentLogDir, yamlName);
8907
+ if (existsSync15(yamlFull))
8908
+ continue;
8909
+ const raw = readFileSync17(fullPath, "utf8");
8910
+ const parsed = parseLegacyAgentLog(raw);
8911
+ const yamlOut = yaml3.dump(parsed, { lineWidth: -1, noRefs: true });
8912
+ pendingWrites.push({ path: yamlFull, content: yamlOut });
8913
+ pendingDeletes.push({ path: fullPath });
8914
+ changed.push(`agent-log/${f} -> agent-log/${yamlName}`);
8915
+ }
8916
+ }
8917
+ function migrateOne(changeId, changeDir, enableContextGovernance) {
8918
+ const changed = [];
8919
+ const warnings = [];
8920
+ const pending = [];
8921
+ const deletes = [];
8922
+ let detectedTier = null;
8923
+ const classifPath = join18(changeDir, "change-classification.md");
8924
+ if (existsSync15(classifPath)) {
8925
+ const content = readFileSync17(classifPath, "utf8");
8926
+ const hasNewTierFormat = /^## Tier\s*\n\s*-\s*\d\s*$/m.test(content);
8927
+ const oldMatch = content.match(/\*\*Tier[:\*]+\s*(?:Tier\s*)?(\d)/i) ?? content.match(/^-?\s*Tier:\s*(?:Tier\s*)?(\d)/mi);
8928
+ if (oldMatch)
8929
+ detectedTier = oldMatch[1];
8930
+ if (!hasNewTierFormat) {
8931
+ if (detectedTier) {
8932
+ const addition = `
8933
+ ## Tier
8934
+ - ${detectedTier}
8935
+ `;
8936
+ if (!content.includes("\n## Tier\n")) {
8937
+ changed.push(
8938
+ `change-classification.md: appended "## Tier\\n- ${detectedTier}" (converted from old format)`
8939
+ );
8940
+ pending.push({ path: classifPath, content: content + addition });
8941
+ }
8942
+ } else {
8943
+ warnings.push(
8944
+ "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."
8945
+ );
8793
8946
  }
8947
+ } else {
8948
+ const structured = content.match(/^## Tier\s*\n\s*-\s*(\d)\s*$/m);
8949
+ if (structured)
8950
+ detectedTier = structured[1];
8794
8951
  }
8795
8952
  }
8796
- }
8797
- function parseAndExtract(source, opts) {
8798
- let ast;
8799
- try {
8800
- ast = parseSourceWithPlugins(source, opts.plugins);
8801
- } catch {
8802
- return null;
8803
- }
8804
- const buckets = {
8805
- imports: [],
8806
- constants: [],
8807
- functions: [],
8808
- classes: [],
8809
- interfaces: [],
8810
- types: [],
8811
- enums: []
8812
- };
8813
- for (const stmt of ast.program.body) {
8814
- processStatement(stmt, buckets, !!opts.extractTsTypes);
8953
+ migrateTasksFile(changeId, changeDir, enableContextGovernance, detectedTier, changed, warnings, pending, deletes);
8954
+ migrateAgentLogs(changeDir, changed, pending, deletes);
8955
+ const manifestPath = join18(changeDir, "context-manifest.md");
8956
+ if (!existsSync15(manifestPath)) {
8957
+ changed.push(enableContextGovernance ? "context-manifest.md: added context-governance v1 manifest scaffold" : "context-manifest.md: added legacy context manifest scaffold");
8958
+ pending.push({
8959
+ path: manifestPath,
8960
+ content: enableContextGovernance ? buildContextGovernedManifest(changeId) : buildLegacyContextManifest(changeId)
8961
+ });
8962
+ } else if (enableContextGovernance) {
8963
+ warnings.push("context-manifest.md already exists \u2014 review allowed paths before relying on context-governance: v1");
8815
8964
  }
8816
- return buckets;
8817
- }
8818
- function countSourceLines(source) {
8819
- if (source === "")
8820
- return 0;
8821
- return source.split(/\r?\n/).length - (source.endsWith("\n") || source.endsWith("\r\n") ? 1 : 0);
8822
- }
8823
- function parseJsSource(source, relPath) {
8824
- const total_lines = countSourceLines(source);
8825
- const r = parseAndExtract(source, { plugins: JS_PLUGINS, extractTsTypes: false });
8826
- if (!r)
8827
- return null;
8828
- return {
8829
- path: relPath,
8830
- total_lines,
8831
- imports: r.imports,
8832
- constants: r.constants,
8833
- classes: r.classes,
8834
- functions: r.functions
8835
- };
8965
+ return { result: { changed, warnings }, pending, deletes };
8836
8966
  }
8837
- var COMMON_PLUGINS, JS_PLUGINS, JavaScriptScanner, jsScanner;
8838
- var init_javascript = __esm({
8839
- "src/code-map/scanners/javascript.ts"() {
8840
- "use strict";
8841
- init_common();
8842
- COMMON_PLUGINS = [
8843
- "classProperties",
8844
- "classPrivateProperties",
8845
- "classPrivateMethods",
8846
- "decorators-legacy",
8847
- "topLevelAwait",
8848
- "dynamicImport",
8849
- "optionalChaining",
8850
- "nullishCoalescingOperator",
8851
- "objectRestSpread",
8852
- "asyncGenerators",
8853
- "numericSeparator",
8854
- "logicalAssignment"
8855
- ];
8856
- JS_PLUGINS = ["jsx", ...COMMON_PLUGINS];
8857
- JavaScriptScanner = class {
8858
- extensions = [".js"];
8859
- async scan(absolutePath, repoRoot) {
8860
- let source;
8861
- try {
8862
- source = readFileSync14(absolutePath, "utf8");
8863
- } catch (err) {
8864
- throw err;
8865
- }
8866
- if (isBinary(source)) {
8867
- return null;
8868
- }
8869
- const relPath = canonicalRelPath(absolutePath, repoRoot);
8870
- return parseJsSource(source, relPath);
8967
+ function commitWritesAtomically(pending, deletes) {
8968
+ const renames = [];
8969
+ try {
8970
+ for (const write of pending) {
8971
+ const tmp = `${write.path}.cdd-migrate.tmp`;
8972
+ writeFileSync8(tmp, write.content, "utf8");
8973
+ renames.push({ tmp, final: write.path });
8974
+ }
8975
+ } catch (err) {
8976
+ for (const r of renames) {
8977
+ try {
8978
+ rmSync2(r.tmp, { force: true });
8979
+ } catch {
8871
8980
  }
8872
- };
8873
- jsScanner = new JavaScriptScanner();
8981
+ }
8982
+ throw err;
8874
8983
  }
8875
- });
8876
-
8877
- // src/code-map/scanners/typescript.ts
8878
- var typescript_exports = {};
8879
- __export(typescript_exports, {
8880
- tsScanner: () => tsScanner
8881
- });
8882
- import { readFileSync as readFileSync15 } from "fs";
8883
- var TypeScriptScanner, tsScanner;
8884
- var init_typescript = __esm({
8885
- "src/code-map/scanners/typescript.ts"() {
8886
- "use strict";
8887
- init_common();
8888
- init_javascript();
8889
- TypeScriptScanner = class {
8890
- extensions = [".ts", ".tsx"];
8891
- async scan(absolutePath, repoRoot) {
8892
- let source;
8893
- try {
8894
- source = readFileSync15(absolutePath, "utf8");
8895
- } catch (err) {
8896
- throw err;
8897
- }
8898
- if (isBinary(source)) {
8899
- return null;
8900
- }
8901
- const isTsx = absolutePath.toLowerCase().endsWith(".tsx");
8902
- const plugins = isTsx ? ["typescript", "jsx", ...COMMON_PLUGINS] : ["typescript", ...COMMON_PLUGINS];
8903
- const r = parseAndExtract(source, { plugins, extractTsTypes: true });
8904
- if (!r)
8905
- return null;
8906
- const relPath = canonicalRelPath(absolutePath, repoRoot);
8907
- const total_lines = countSourceLines(source);
8908
- return {
8909
- path: relPath,
8910
- total_lines,
8911
- imports: r.imports,
8912
- constants: r.constants,
8913
- classes: r.classes,
8914
- functions: r.functions,
8915
- interfaces: r.interfaces,
8916
- types: r.types,
8917
- enums: r.enums
8918
- };
8919
- }
8920
- };
8921
- tsScanner = new TypeScriptScanner();
8984
+ for (const r of renames) {
8985
+ renameSync(r.tmp, r.final);
8922
8986
  }
8923
- });
8924
-
8925
- // src/code-map/scanners/vue.ts
8926
- var vue_exports = {};
8927
- __export(vue_exports, {
8928
- vueScanner: () => vueScanner
8929
- });
8930
- import { readFileSync as readFileSync16 } from "fs";
8931
- import { parse as parse2 } from "@vue/compiler-sfc";
8932
- var VueScanner, vueScanner;
8933
- var init_vue = __esm({
8934
- "src/code-map/scanners/vue.ts"() {
8935
- "use strict";
8936
- init_common();
8937
- init_javascript();
8938
- VueScanner = class {
8939
- extensions = [".vue"];
8940
- async scan(absolutePath, repoRoot) {
8941
- let source;
8942
- try {
8943
- source = readFileSync16(absolutePath, "utf8");
8944
- } catch (err) {
8945
- throw err;
8946
- }
8947
- if (isBinary(source)) {
8948
- return null;
8949
- }
8950
- const relPath = canonicalRelPath(absolutePath, repoRoot);
8951
- const total_lines = source.split(/\r?\n/).length - (source.endsWith("\n") || source.endsWith("\r\n") ? 1 : 0);
8952
- const { descriptor } = parse2(source, { filename: absolutePath });
8953
- const allImports = [];
8954
- const allConstants = [];
8955
- const allFunctions = [];
8956
- const allClasses = [];
8957
- const warnings = [];
8958
- const scriptBlocks = [descriptor.script, descriptor.scriptSetup].filter(Boolean);
8959
- if (scriptBlocks.length === 0) {
8960
- return {
8961
- path: relPath,
8962
- total_lines,
8963
- imports: [],
8964
- constants: [],
8965
- classes: [],
8966
- functions: []
8967
- };
8968
- }
8969
- for (const block of scriptBlocks) {
8970
- if (!block)
8971
- continue;
8972
- if (block.lang === "ts" || block.lang === "tsx") {
8973
- warnings.push({
8974
- path: relPath,
8975
- message: `skipping <script lang=${block.lang}> block (TypeScript not supported in v1)`
8976
- });
8977
- continue;
8978
- }
8979
- const blockContent = block.content;
8980
- const lineOffset = block.loc.start.line;
8981
- const scriptEntry = parseJsSource(blockContent, relPath);
8982
- if (!scriptEntry) {
8983
- warnings.push({ path: relPath, message: "parse error in script block" });
8984
- continue;
8985
- }
8986
- const offset = lineOffset - 1;
8987
- for (const imp of scriptEntry.imports) {
8988
- allImports.push({ ...imp, line: imp.line + offset });
8989
- }
8990
- for (const c of scriptEntry.constants) {
8991
- allConstants.push({ ...c, line: c.line + offset });
8992
- }
8993
- for (const fn of scriptEntry.functions) {
8994
- allFunctions.push({
8995
- ...fn,
8996
- lines: [fn.lines[0] + offset, fn.lines[1] + offset]
8997
- });
8998
- }
8999
- for (const cls of scriptEntry.classes) {
9000
- allClasses.push({
9001
- ...cls,
9002
- lines: [cls.lines[0] + offset, cls.lines[1] + offset],
9003
- methods: cls.methods.map((m) => ({
9004
- ...m,
9005
- lines: [m.lines[0] + offset, m.lines[1] + offset]
9006
- }))
9007
- });
9008
- }
8987
+ for (const d of deletes) {
8988
+ try {
8989
+ rmSync2(d.path, { force: true });
8990
+ } catch {
8991
+ }
8992
+ }
8993
+ }
8994
+ async function migrate(changeId, opts = {}) {
8995
+ const cwd = process.cwd();
8996
+ const dryRun = opts.dryRun ?? false;
8997
+ const enableContextGovernance = opts.enableContextGovernance ?? false;
8998
+ const noBackup = opts.noBackup ?? false;
8999
+ const idsToMigrate = [];
9000
+ if (opts.all) {
9001
+ const changesDir = join18(cwd, "specs", "changes");
9002
+ if (!existsSync15(changesDir)) {
9003
+ log.info("No specs/changes/ directory found \u2014 nothing to migrate.");
9004
+ return;
9005
+ }
9006
+ idsToMigrate.push(
9007
+ ...readdirSync8(changesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name)
9008
+ );
9009
+ } else if (changeId) {
9010
+ const specificDir = join18(cwd, "specs", "changes", changeId);
9011
+ if (!existsSync15(specificDir)) {
9012
+ log.error(`Change not found: specs/changes/${changeId}`);
9013
+ process.exit(1);
9014
+ }
9015
+ idsToMigrate.push(changeId);
9016
+ } else {
9017
+ log.error("Usage: cdd-kit migrate <change-id> | cdd-kit migrate --all [--dry-run] [--no-backup]");
9018
+ process.exit(1);
9019
+ }
9020
+ if (idsToMigrate.length === 0) {
9021
+ log.info("No changes found to migrate.");
9022
+ return;
9023
+ }
9024
+ if (dryRun) {
9025
+ log.info("Dry run \u2014 no files will be written.");
9026
+ log.blank();
9027
+ }
9028
+ const sessionStamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
9029
+ let migratedCount = 0;
9030
+ let upToDateCount = 0;
9031
+ const backupRoot = join18(cwd, ".cdd", "migrate-backup", sessionStamp);
9032
+ for (const id of idsToMigrate) {
9033
+ const changeDir = join18(cwd, "specs", "changes", id);
9034
+ if (!existsSync15(changeDir)) {
9035
+ log.warn(` ${id}: directory not found \u2014 skipping`);
9036
+ continue;
9037
+ }
9038
+ const { result, pending, deletes } = migrateOne(id, changeDir, enableContextGovernance);
9039
+ const { changed, warnings } = result;
9040
+ if (changed.length === 0) {
9041
+ log.info(` ${id}: already up to date`);
9042
+ upToDateCount++;
9043
+ for (const w of warnings)
9044
+ log.warn(` ${id}: ${w}`);
9045
+ continue;
9046
+ }
9047
+ if (!dryRun) {
9048
+ try {
9049
+ if (!noBackup)
9050
+ backupChangeDir(cwd, id, sessionStamp);
9051
+ commitWritesAtomically(pending, deletes);
9052
+ } catch (err) {
9053
+ log.error(` ${id}: migration failed \u2014 ${err.message}`);
9054
+ if (!noBackup) {
9055
+ log.error(` ${id}: restore from .cdd/migrate-backup/${sessionStamp}/${id}/`);
9009
9056
  }
9010
- return {
9011
- path: relPath,
9012
- total_lines,
9013
- imports: allImports,
9014
- constants: allConstants,
9015
- classes: allClasses,
9016
- functions: allFunctions
9017
- };
9057
+ process.exit(1);
9018
9058
  }
9019
- };
9020
- vueScanner = new VueScanner();
9059
+ }
9060
+ log.ok(` ${id}: migrated`);
9061
+ for (const c of changed)
9062
+ log.info(` + ${c}`);
9063
+ migratedCount++;
9064
+ for (const w of warnings)
9065
+ log.warn(` ${id}: ${w}`);
9066
+ }
9067
+ log.blank();
9068
+ if (dryRun) {
9069
+ log.info(`Dry run complete: ${migratedCount} change(s) would be updated, ${upToDateCount} already up to date.`);
9070
+ } else {
9071
+ log.ok(`Migration complete: ${migratedCount} updated, ${upToDateCount} already up to date.`);
9072
+ if (migratedCount > 0 && !noBackup) {
9073
+ log.info(`Backup: ${backupRoot}`);
9074
+ if (ensureGitignoreEntry(cwd, ".cdd/migrate-backup/")) {
9075
+ log.info("Added `.cdd/migrate-backup/` to .gitignore");
9076
+ }
9077
+ log.info('Next: git add specs/changes/ && git commit -m "chore: migrate changes to YAML format"');
9078
+ log.info("When stable, remove backup: rm -rf .cdd/migrate-backup/");
9079
+ }
9080
+ }
9081
+ }
9082
+ function ensureGitignoreEntry(cwd, entry) {
9083
+ const path = join18(cwd, ".gitignore");
9084
+ const trimmed = entry.trim();
9085
+ if (!trimmed)
9086
+ return false;
9087
+ const re = new RegExp(`^\\s*${trimmed.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\$&")}\\s*$`, "m");
9088
+ let existing = "";
9089
+ if (existsSync15(path))
9090
+ existing = readFileSync17(path, "utf8");
9091
+ if (re.test(existing))
9092
+ return false;
9093
+ const sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
9094
+ const block = existing.length === 0 ? `# cdd-kit generated backups (do not commit)
9095
+ ${trimmed}
9096
+ ` : `${sep}
9097
+ # cdd-kit generated backups (do not commit)
9098
+ ${trimmed}
9099
+ `;
9100
+ writeFileSync8(path, existing + block, "utf8");
9101
+ return true;
9102
+ }
9103
+ var init_migrate = __esm({
9104
+ "src/commands/migrate.ts"() {
9105
+ "use strict";
9106
+ init_logger();
9021
9107
  }
9022
9108
  });
9023
9109
 
9024
- // src/commands/code-map.ts
9025
- var code_map_exports = {};
9026
- __export(code_map_exports, {
9027
- codeMap: () => codeMap
9110
+ // src/commands/upgrade.ts
9111
+ var upgrade_exports = {};
9112
+ __export(upgrade_exports, {
9113
+ upgrade: () => upgrade
9028
9114
  });
9029
- import { existsSync as existsSync16, mkdirSync as mkdirSync8, readFileSync as readFileSync17, writeFileSync as writeFileSync9 } from "fs";
9030
- import { resolve, dirname as dirname5 } from "path";
9031
- import { createRequire } from "module";
9032
- import { fileURLToPath as fileURLToPath2 } from "url";
9033
- import { join as join19 } from "path";
9034
- async function codeMap(opts) {
9035
- const root = resolve(process.cwd(), opts.path);
9036
- const start = Date.now();
9037
- let cfg;
9038
- try {
9039
- cfg = loadCodeMapConfig(root);
9040
- } catch (err) {
9041
- log.error(`code-map: ${err.message}`);
9042
- return 1;
9043
- }
9044
- const include = [...cfg.include, ...opts.include];
9045
- const exclude = [...cfg.exclude, ...opts.exclude];
9046
- const files = walkRepo(root, { include, exclude });
9047
- const buckets = bucketByExtension(files);
9048
- const result = { entries: [], warnings: [] };
9049
- const tasks = [];
9050
- if (buckets[".py"]?.length) {
9051
- const { pythonScanner: pythonScanner2 } = await Promise.resolve().then(() => (init_python(), python_exports));
9052
- if (pythonScanner2.scanBatch) {
9053
- tasks.push(pythonScanner2.scanBatch(buckets[".py"], root));
9115
+ import { existsSync as existsSync16, mkdirSync as mkdirSync8, readdirSync as readdirSync9, copyFileSync as copyFileSync3, readFileSync as readFileSync18, writeFileSync as writeFileSync9 } from "fs";
9116
+ import { dirname as dirname5, join as join19, relative as relative6 } from "path";
9117
+ function planMissingFiles(srcDir, destDir, label, planned) {
9118
+ if (!existsSync16(srcDir))
9119
+ return;
9120
+ for (const entry of readdirSync9(srcDir, { withFileTypes: true })) {
9121
+ const src = join19(srcDir, entry.name);
9122
+ const dest = join19(destDir, entry.name);
9123
+ if (entry.isDirectory()) {
9124
+ planMissingFiles(src, dest, join19(label, entry.name), planned);
9125
+ continue;
9126
+ }
9127
+ if (!existsSync16(dest)) {
9128
+ planned.push({ src, dest, rel: join19(label, relative6(srcDir, src)) });
9054
9129
  }
9055
9130
  }
9056
- const jsFiles = [
9057
- ...buckets[".js"] ?? [],
9058
- ...buckets[".jsx"] ?? [],
9059
- ...buckets[".mjs"] ?? [],
9060
- ...buckets[".cjs"] ?? []
9061
- ];
9062
- if (jsFiles.length) {
9063
- const { jsScanner: jsScanner2 } = await Promise.resolve().then(() => (init_javascript(), javascript_exports));
9064
- tasks.push(scanInProcess(jsScanner2, jsFiles, root));
9131
+ }
9132
+ function planProviderGuidance(cwd, provider, planned) {
9133
+ if (provider === "claude" || provider === "both") {
9134
+ if (!existsSync16(join19(cwd, "CLAUDE.md"))) {
9135
+ planned.push({ src: ASSET.claudeTemplate, dest: join19(cwd, "CLAUDE.md"), rel: "CLAUDE.md" });
9136
+ }
9137
+ if (!existsSync16(join19(cwd, "AGENTS.md"))) {
9138
+ planned.push({ src: ASSET.agentsTemplate, dest: join19(cwd, "AGENTS.md"), rel: "AGENTS.md" });
9139
+ }
9065
9140
  }
9066
- const tsFiles = [
9067
- ...buckets[".ts"] ?? [],
9068
- ...buckets[".tsx"] ?? []
9069
- ];
9070
- if (tsFiles.length) {
9071
- const { tsScanner: tsScanner2 } = await Promise.resolve().then(() => (init_typescript(), typescript_exports));
9072
- tasks.push(scanInProcess(tsScanner2, tsFiles, root));
9141
+ if ((provider === "codex" || provider === "both") && !existsSync16(join19(cwd, "CODEX.md"))) {
9142
+ planned.push({ src: ASSET.codexTemplate, dest: join19(cwd, "CODEX.md"), rel: "CODEX.md" });
9073
9143
  }
9074
- if (buckets[".vue"]?.length) {
9075
- const { vueScanner: vueScanner2 } = await Promise.resolve().then(() => (init_vue(), vue_exports));
9076
- tasks.push(scanInProcess(vueScanner2, buckets[".vue"], root));
9144
+ }
9145
+ function applyCopy(plan) {
9146
+ for (const item of plan) {
9147
+ mkdirSync8(dirname5(item.dest), { recursive: true });
9148
+ copyFileSync3(item.src, item.dest);
9077
9149
  }
9078
- for (const r of await Promise.all(tasks)) {
9079
- result.entries.push(...r.entries);
9080
- result.warnings.push(...r.warnings);
9150
+ }
9151
+ async function upgrade(opts = {}) {
9152
+ const cwd = process.cwd();
9153
+ const requestedProvider = opts.provider ?? "auto";
9154
+ if (!validateProviderOption(requestedProvider)) {
9155
+ log.error(`Invalid provider: ${requestedProvider}. Use auto, claude, codex, or both.`);
9156
+ process.exit(1);
9081
9157
  }
9082
- result.entries.sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
9083
- for (const e of result.entries) {
9084
- if (e.total_lines > opts.maxLines) {
9085
- result.warnings.push({
9086
- path: e.path,
9087
- message: `file exceeds --max-lines (${e.total_lines} > ${opts.maxLines})`
9158
+ const provider = inferProvider(cwd, requestedProvider);
9159
+ const plan = [];
9160
+ planMissingFiles(ASSET.contracts, join19(cwd, "contracts"), "contracts", plan);
9161
+ planMissingFiles(ASSET.specsTemplates, join19(cwd, "specs", "templates"), "specs/templates", plan);
9162
+ planMissingFiles(ASSET.testsTemplates, join19(cwd, "tests", "templates"), "tests/templates", plan);
9163
+ planMissingFiles(ASSET.ci, join19(cwd, "ci"), "ci", plan);
9164
+ planMissingFiles(ASSET.githubWorkflows, join19(cwd, ".github", "workflows"), ".github/workflows", plan);
9165
+ planMissingFiles(ASSET.cddConfig, join19(cwd, ".cdd"), ".cdd", plan);
9166
+ planProviderGuidance(cwd, provider, plan);
9167
+ log.blank();
9168
+ log.info(`Upgrade provider: ${provider}`);
9169
+ if (plan.length === 0) {
9170
+ log.ok("No missing cdd-kit project files found.");
9171
+ if (opts.migrateChanges) {
9172
+ log.blank();
9173
+ log.info("Running change migration flow...");
9174
+ await migrate(void 0, {
9175
+ all: true,
9176
+ dryRun: !opts.yes,
9177
+ enableContextGovernance: opts.enableContextGovernance
9088
9178
  });
9089
9179
  }
9180
+ log.blank();
9181
+ return;
9090
9182
  }
9091
- const yamlBody = renderYaml(result.entries, { generator: `cdd-kit ${_pkg.version}` });
9092
- const totalSrc = result.entries.reduce((s, e) => s + e.total_lines, 0);
9093
- const mapLines = yamlBody.split("\n").length;
9094
- const compression = totalSrc === 0 ? 0 : totalSrc / mapLines;
9095
- const summaryLine = `scanned ${result.entries.length} files, ${totalSrc} src lines -> ${opts.out} (${mapLines} lines, compression ${compression.toFixed(1)}x)`;
9096
- for (const w of result.warnings) {
9097
- log.warn(`${w.path}: ${w.message}`);
9183
+ log.info(`${plan.length} missing file(s) detected:`);
9184
+ for (const item of plan)
9185
+ log.dim(` + ${item.rel.replace(/\\/g, "/")}`);
9186
+ if (!opts.yes) {
9187
+ log.blank();
9188
+ log.info("Dry run only. Re-run with --yes to write missing files.");
9189
+ if (opts.migrateChanges) {
9190
+ log.blank();
9191
+ log.info("Previewing existing change migration because --migrate-changes was requested.");
9192
+ await migrate(void 0, {
9193
+ all: true,
9194
+ dryRun: true,
9195
+ enableContextGovernance: opts.enableContextGovernance
9196
+ });
9197
+ }
9198
+ log.blank();
9199
+ return;
9098
9200
  }
9099
- if (opts.check) {
9100
- const existing = existsSync16(opts.out) ? readFileSync17(opts.out, "utf8") : "";
9101
- const normalize = (s) => s.replace(/^# generated: [^\n]+\n/m, "# generated: <normalized>\n");
9102
- if (normalize(existing) !== normalize(yamlBody)) {
9103
- log.error(`code-map out of date: ${opts.out} would change. Run \`cdd-kit code-map\` to regenerate.`);
9104
- return 1;
9201
+ applyCopy(plan);
9202
+ const modelPolicyPath = join19(cwd, ".cdd", "model-policy.json");
9203
+ if (existsSync16(modelPolicyPath)) {
9204
+ let existing = {};
9205
+ try {
9206
+ existing = JSON.parse(readFileSync18(modelPolicyPath, "utf8"));
9207
+ } catch {
9105
9208
  }
9106
- log.ok(`code-map up to date: ${opts.out}`);
9107
- return 0;
9209
+ const merged = {
9210
+ ...existing,
9211
+ provider,
9212
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
9213
+ roles: existing.roles && typeof existing.roles === "object" ? existing.roles : {}
9214
+ };
9215
+ writeFileSync9(modelPolicyPath, JSON.stringify(merged, null, 2) + "\n", "utf8");
9108
9216
  }
9109
- mkdirSync8(dirname5(opts.out), { recursive: true });
9110
- writeFileSync9(opts.out, yamlBody, "utf8");
9111
- log.ok(`${summaryLine} (${Date.now() - start}ms)`);
9112
- return 0;
9217
+ log.blank();
9218
+ log.ok(`Upgrade complete: ${plan.length} missing file(s) added.`);
9219
+ log.info("Existing project guidance and contracts were preserved.");
9220
+ if (opts.migrateChanges) {
9221
+ log.blank();
9222
+ log.info("Running change migration flow...");
9223
+ await migrate(void 0, {
9224
+ all: true,
9225
+ dryRun: false,
9226
+ enableContextGovernance: opts.enableContextGovernance
9227
+ });
9228
+ }
9229
+ log.blank();
9113
9230
  }
9114
- var _require, _pkgPath, _pkg;
9115
- var init_code_map = __esm({
9116
- "src/commands/code-map.ts"() {
9231
+ var init_upgrade = __esm({
9232
+ "src/commands/upgrade.ts"() {
9117
9233
  "use strict";
9234
+ init_paths();
9118
9235
  init_logger();
9119
- init_yaml_writer();
9120
- init_orchestrator();
9121
- init_config();
9122
- _require = createRequire(import.meta.url);
9123
- _pkgPath = join19(fileURLToPath2(import.meta.url), "..", "..", "..", "package.json");
9124
- _pkg = JSON.parse(readFileSync17(_pkgPath, "utf8"));
9236
+ init_provider();
9237
+ init_migrate();
9125
9238
  }
9126
9239
  });
9127
9240
 
@@ -9130,11 +9243,11 @@ var refresh_exports = {};
9130
9243
  __export(refresh_exports, {
9131
9244
  refresh: () => refresh
9132
9245
  });
9133
- import { existsSync as existsSync17, mkdirSync as mkdirSync9, readdirSync as readdirSync10, copyFileSync as copyFileSync4, readFileSync as readFileSync18, writeFileSync as writeFileSync10 } from "fs";
9134
- import { dirname as dirname6, join as join20, relative as relative5 } from "path";
9135
- import { createHash as createHash4 } from "crypto";
9246
+ import { existsSync as existsSync17, mkdirSync as mkdirSync9, readdirSync as readdirSync10, copyFileSync as copyFileSync4, readFileSync as readFileSync19, writeFileSync as writeFileSync10 } from "fs";
9247
+ import { dirname as dirname6, join as join20, relative as relative7 } from "path";
9248
+ import { createHash as createHash5 } from "crypto";
9136
9249
  function fileHash2(filePath) {
9137
- return createHash4("sha256").update(readFileSync18(filePath)).digest("hex");
9250
+ return createHash5("sha256").update(readFileSync19(filePath)).digest("hex");
9138
9251
  }
9139
9252
  function planForceRefresh(srcDir, destDir, sectionLabel) {
9140
9253
  const plan = [];
@@ -9156,7 +9269,7 @@ function planForceRefresh(srcDir, destDir, sectionLabel) {
9156
9269
  }
9157
9270
  if (!item.isFile())
9158
9271
  continue;
9159
- const rel = join20(sectionLabel, relative5(srcDir, sp)).replace(/\\/g, "/");
9272
+ const rel = join20(sectionLabel, relative7(srcDir, sp)).replace(/\\/g, "/");
9160
9273
  if (!existsSync17(dp)) {
9161
9274
  plan.push({ src: sp, dest: dp, rel, action: "add" });
9162
9275
  } else if (fileHash2(sp) !== fileHash2(dp)) {
@@ -9178,6 +9291,27 @@ function planSingleFile(src, dest, rel) {
9178
9291
  return { src, dest, rel, action: "overwrite" };
9179
9292
  return { src, dest, rel, action: "skip" };
9180
9293
  }
9294
+ function ensureGitignoreEntry2(cwd, entry) {
9295
+ const path = join20(cwd, ".gitignore");
9296
+ const trimmed = entry.trim();
9297
+ if (!trimmed)
9298
+ return false;
9299
+ const re = new RegExp(`^\\s*${trimmed.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\$&")}\\s*$`, "m");
9300
+ let existing = "";
9301
+ if (existsSync17(path))
9302
+ existing = readFileSync19(path, "utf8");
9303
+ if (re.test(existing))
9304
+ return false;
9305
+ const sep = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
9306
+ const block = existing.length === 0 ? `# cdd-kit generated backups (do not commit)
9307
+ ${trimmed}
9308
+ ` : `${sep}
9309
+ # cdd-kit generated backups (do not commit)
9310
+ ${trimmed}
9311
+ `;
9312
+ writeFileSync10(path, existing + block, "utf8");
9313
+ return true;
9314
+ }
9181
9315
  function applyPlan(plan, backupRoot) {
9182
9316
  let added = 0;
9183
9317
  let overwritten = 0;
@@ -9221,7 +9355,7 @@ function resyncModelPolicy(cwd) {
9221
9355
  const desired = {};
9222
9356
  const agentFiles = readdirSync10(AGENTS_HOME, { withFileTypes: true }).filter((d) => d.isFile() && d.name.endsWith(".md"));
9223
9357
  for (const f of agentFiles) {
9224
- const content = readFileSync18(join20(AGENTS_HOME, f.name), "utf8");
9358
+ const content = readFileSync19(join20(AGENTS_HOME, f.name), "utf8");
9225
9359
  const fm = parseAgentFrontmatter(content);
9226
9360
  if (fm.name && fm.model)
9227
9361
  desired[fm.name] = fm.model;
@@ -9231,7 +9365,7 @@ function resyncModelPolicy(cwd) {
9231
9365
  let existing = {};
9232
9366
  if (existsSync17(policyPath)) {
9233
9367
  try {
9234
- existing = JSON.parse(readFileSync18(policyPath, "utf8"));
9368
+ existing = JSON.parse(readFileSync19(policyPath, "utf8"));
9235
9369
  } catch {
9236
9370
  }
9237
9371
  }
@@ -9335,7 +9469,10 @@ async function refresh(opts) {
9335
9469
  templateOverwritten = result.overwritten;
9336
9470
  log.ok(` applied: +${templateAdded} added, ~${templateOverwritten} overwritten`);
9337
9471
  if (templateOverwritten > 0) {
9338
- log.info(` backup saved to: ${relative5(cwd, backupRoot).replace(/\\/g, "/")}`);
9472
+ log.info(` backup saved to: ${relative7(cwd, backupRoot).replace(/\\/g, "/")}`);
9473
+ if (ensureGitignoreEntry2(cwd, ".cdd/.refresh-backup/")) {
9474
+ log.info(" added `.cdd/.refresh-backup/` to .gitignore");
9475
+ }
9339
9476
  }
9340
9477
  } else {
9341
9478
  log.dim(" (dry-run \u2014 no changes written)");
@@ -9408,7 +9545,7 @@ async function refresh(opts) {
9408
9545
  if (apply) {
9409
9546
  log.ok("refresh complete.");
9410
9547
  if (backupRoot) {
9411
- log.info(`Backup of overwritten templates: ${relative5(cwd, backupRoot).replace(/\\/g, "/")}`);
9548
+ log.info(`Backup of overwritten templates: ${relative7(cwd, backupRoot).replace(/\\/g, "/")}`);
9412
9549
  }
9413
9550
  log.info("Next: review changes with `git diff`, then commit:");
9414
9551
  log.dim(" git add .cdd/code-map.yml .cdd/model-policy.json specs/templates tests/templates ci-templates .github/workflows");
@@ -9437,9 +9574,9 @@ var doctor_exports = {};
9437
9574
  __export(doctor_exports, {
9438
9575
  doctor: () => doctor
9439
9576
  });
9440
- import { existsSync as existsSync18, readdirSync as readdirSync11, readFileSync as readFileSync19 } from "fs";
9441
- import { createHash as createHash5 } from "crypto";
9442
- import { join as join21 } from "path";
9577
+ import { existsSync as existsSync18, readdirSync as readdirSync11, readFileSync as readFileSync20 } from "fs";
9578
+ import { createHash as createHash6 } from "crypto";
9579
+ import { join as join21, relative as relative8 } from "path";
9443
9580
  function fileExists(cwd, relPath) {
9444
9581
  return existsSync18(join21(cwd, relPath));
9445
9582
  }
@@ -9457,19 +9594,22 @@ function findFiles(dir, predicate, found = []) {
9457
9594
  }
9458
9595
  function sha256OfFile3(path) {
9459
9596
  try {
9460
- return createHash5("sha256").update(readFileSync19(path)).digest("hex");
9597
+ return createHash6("sha256").update(readFileSync20(path)).digest("hex");
9461
9598
  } catch {
9462
9599
  return "";
9463
9600
  }
9464
9601
  }
9465
- function inputDigest(paths) {
9466
- const combined = paths.slice().sort().map((p) => `${p}:${sha256OfFile3(p)}`).join("\n");
9467
- return createHash5("sha256").update(combined).digest("hex");
9602
+ function inputDigest(paths, cwd) {
9603
+ const combined = paths.slice().sort().map((p) => {
9604
+ const rel = relative8(cwd, p).replace(/\\/g, "/");
9605
+ return `${rel}:${sha256OfFile3(p)}`;
9606
+ }).join("\n");
9607
+ return createHash6("sha256").update(combined).digest("hex");
9468
9608
  }
9469
9609
  function readContextIndexMetadata(filePath) {
9470
9610
  if (!existsSync18(filePath))
9471
9611
  return {};
9472
- const text = readFileSync19(filePath, "utf8");
9612
+ const text = readFileSync20(filePath, "utf8");
9473
9613
  const out = {};
9474
9614
  const digestMatch = text.match(/^inputs-digest:\s*([a-f0-9]+)/m);
9475
9615
  if (digestMatch)
@@ -9497,7 +9637,7 @@ function checkContextFreshness(cwd) {
9497
9637
  }
9498
9638
  const projectMapMeta = readContextIndexMetadata(projectMap);
9499
9639
  const contractsIndexMeta = readContextIndexMetadata(contractsIndex);
9500
- const projectInputDigest = inputDigest([contextPolicy].filter(existsSync18));
9640
+ const projectInputDigest = inputDigest([contextPolicy].filter(existsSync18), cwd);
9501
9641
  if (projectMapMeta.inputsDigest === void 0) {
9502
9642
  findings.push({
9503
9643
  level: "warning",
@@ -9509,7 +9649,7 @@ function checkContextFreshness(cwd) {
9509
9649
  message: "specs/context/project-map.md inputs changed (.cdd/context-policy.json); re-run cdd-kit context-scan"
9510
9650
  });
9511
9651
  }
9512
- const contractsInputDigest = inputDigest(contractFiles);
9652
+ const contractsInputDigest = inputDigest(contractFiles, cwd);
9513
9653
  if (contractsIndexMeta.inputsDigest === void 0) {
9514
9654
  findings.push({
9515
9655
  level: "warning",
@@ -9534,7 +9674,7 @@ function checkContextFreshness(cwd) {
9534
9674
  }
9535
9675
  function readAgentModel(path) {
9536
9676
  try {
9537
- const text = readFileSync19(path, "utf8");
9677
+ const text = readFileSync20(path, "utf8");
9538
9678
  const m = text.match(/^model:\s*(\S+)/m);
9539
9679
  return m ? m[1] : null;
9540
9680
  } catch {
@@ -9547,7 +9687,7 @@ function checkModelPolicyDrift(cwd) {
9547
9687
  return [];
9548
9688
  let policy;
9549
9689
  try {
9550
- policy = JSON.parse(readFileSync19(policyPath, "utf8"));
9690
+ policy = JSON.parse(readFileSync20(policyPath, "utf8"));
9551
9691
  } catch {
9552
9692
  return [{ level: "warning", message: ".cdd/model-policy.json is not valid JSON" }];
9553
9693
  }
@@ -9648,7 +9788,7 @@ function checkCodeMap(cwd) {
9648
9788
  const more = probe.staleCount > 3 ? ` (+${probe.staleCount - 3} more)` : "";
9649
9789
  findings.push({ level: "warning", message: `code-map stale: ${top}${more}; run \`cdd-kit code-map\`` });
9650
9790
  }
9651
- const text = readFileSync19(mapPath, "utf8");
9791
+ const text = readFileSync20(mapPath, "utf8");
9652
9792
  const m = text.match(/^# files: (\d+), src-lines: (\d+), map-lines: (\d+), compression: ([\d.]+)x/m);
9653
9793
  if (m) {
9654
9794
  findings.push({ level: "ok", message: `code-map: ${m[1]} files, ${m[4]}x compression` });
@@ -9728,7 +9868,7 @@ async function attemptAutoFixes(cwd, report) {
9728
9868
  try {
9729
9869
  let existing = {};
9730
9870
  try {
9731
- existing = JSON.parse(readFileSync19(policyPath, "utf8"));
9871
+ existing = JSON.parse(readFileSync20(policyPath, "utf8"));
9732
9872
  } catch {
9733
9873
  }
9734
9874
  const merged = {
@@ -9829,7 +9969,7 @@ var lint_agents_exports = {};
9829
9969
  __export(lint_agents_exports, {
9830
9970
  lintAgents: () => lintAgents
9831
9971
  });
9832
- import { readdirSync as readdirSync12, readFileSync as readFileSync20 } from "fs";
9972
+ import { readdirSync as readdirSync12, readFileSync as readFileSync21 } from "fs";
9833
9973
  import { join as join22 } from "path";
9834
9974
  import { load as yamlLoad2 } from "js-yaml";
9835
9975
  function extractRequiredArtifactsSection(content) {
@@ -9884,7 +10024,7 @@ async function lintAgents(opts) {
9884
10024
  const filePath = join22(agentsDir, filename);
9885
10025
  let content;
9886
10026
  try {
9887
- content = readFileSync20(filePath, "utf8");
10027
+ content = readFileSync21(filePath, "utf8");
9888
10028
  } catch {
9889
10029
  violations.push({
9890
10030
  file: filename,
@@ -9997,7 +10137,7 @@ __export(archive_exports, {
9997
10137
  archive: () => archive
9998
10138
  });
9999
10139
  import { join as join23 } from "path";
10000
- 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";
10140
+ import { existsSync as existsSync19, mkdirSync as mkdirSync10, renameSync as renameSync2, readFileSync as readFileSync22, writeFileSync as writeFileSync11, appendFileSync, cpSync as cpSync3, rmSync as rmSync3 } from "fs";
10001
10141
  import yaml4 from "js-yaml";
10002
10142
  async function archive(changeId) {
10003
10143
  const cwd = process.cwd();
@@ -10017,7 +10157,7 @@ async function archive(changeId) {
10017
10157
  const tasksPath = join23(changeDir, "tasks.yml");
10018
10158
  if (existsSync19(tasksPath)) {
10019
10159
  try {
10020
- const raw = readFileSync21(tasksPath, "utf8");
10160
+ const raw = readFileSync22(tasksPath, "utf8");
10021
10161
  const data = yaml4.load(raw);
10022
10162
  if (data?.status === "gate-blocked") {
10023
10163
  log.warn("tasks.yml has status: gate-blocked \u2014 archiving anyway (change was paused).");
@@ -10073,7 +10213,7 @@ __export(abandon_exports, {
10073
10213
  abandon: () => abandon
10074
10214
  });
10075
10215
  import { join as join24 } from "path";
10076
- import { existsSync as existsSync20, readFileSync as readFileSync22, writeFileSync as writeFileSync12, appendFileSync as appendFileSync2, mkdirSync as mkdirSync11 } from "fs";
10216
+ import { existsSync as existsSync20, readFileSync as readFileSync23, writeFileSync as writeFileSync12, appendFileSync as appendFileSync2, mkdirSync as mkdirSync11 } from "fs";
10077
10217
  import yaml5 from "js-yaml";
10078
10218
  async function abandon(changeId, opts) {
10079
10219
  const cwd = process.cwd();
@@ -10084,7 +10224,7 @@ async function abandon(changeId, opts) {
10084
10224
  process.exit(1);
10085
10225
  }
10086
10226
  if (existsSync20(tasksPath)) {
10087
- const raw = readFileSync22(tasksPath, "utf8");
10227
+ const raw = readFileSync23(tasksPath, "utf8");
10088
10228
  const data = yaml5.load(raw) ?? {};
10089
10229
  data["status"] = "abandoned";
10090
10230
  if (!data["change-id"]) {
@@ -10127,7 +10267,7 @@ __export(list_changes_exports, {
10127
10267
  listChanges: () => listChanges
10128
10268
  });
10129
10269
  import { join as join25 } from "path";
10130
- import { existsSync as existsSync21, readdirSync as readdirSync13, readFileSync as readFileSync23 } from "fs";
10270
+ import { existsSync as existsSync21, readdirSync as readdirSync13, readFileSync as readFileSync24 } from "fs";
10131
10271
  import yaml6 from "js-yaml";
10132
10272
  async function listChanges() {
10133
10273
  const cwd = process.cwd();
@@ -10147,7 +10287,7 @@ async function listChanges() {
10147
10287
  let pending = 0;
10148
10288
  if (existsSync21(tasksPath)) {
10149
10289
  try {
10150
- const raw = readFileSync23(tasksPath, "utf8");
10290
+ const raw = readFileSync24(tasksPath, "utf8");
10151
10291
  const data = yaml6.load(raw);
10152
10292
  if (data?.status)
10153
10293
  status = data.status;
@@ -10178,7 +10318,7 @@ __export(context_exports, {
10178
10318
  rejectContextExpansion: () => rejectContextExpansion,
10179
10319
  requestContextExpansion: () => requestContextExpansion
10180
10320
  });
10181
- import { existsSync as existsSync22, readFileSync as readFileSync24, writeFileSync as writeFileSync13 } from "fs";
10321
+ import { existsSync as existsSync22, readFileSync as readFileSync25, writeFileSync as writeFileSync13 } from "fs";
10182
10322
  import { join as join26 } from "path";
10183
10323
  function normalizePath(path) {
10184
10324
  return path.replace(/\\/g, "/").replace(/^\.\//, "").trim();
@@ -10201,7 +10341,7 @@ function readManifest(changeId) {
10201
10341
  log.error(`context manifest not found: specs/changes/${changeId}/context-manifest.md`);
10202
10342
  process.exit(1);
10203
10343
  }
10204
- return readFileSync24(manifestPath, "utf8");
10344
+ return readFileSync25(manifestPath, "utf8");
10205
10345
  }
10206
10346
  function writeManifest(changeId, content) {
10207
10347
  writeFileSync13(manifestPathFor(changeId), content.endsWith("\n") ? content : `${content}
@@ -10429,7 +10569,7 @@ var init_context = __esm({
10429
10569
  });
10430
10570
 
10431
10571
  // src/cli/index.ts
10432
- import { readFileSync as readFileSync25 } from "fs";
10572
+ import { readFileSync as readFileSync26 } from "fs";
10433
10573
  import { fileURLToPath as fileURLToPath3 } from "url";
10434
10574
  import { dirname as dirname7, join as join27 } from "path";
10435
10575
  import { Command } from "commander";
@@ -10851,7 +10991,7 @@ init_update();
10851
10991
 
10852
10992
  // src/commands/new-change.ts
10853
10993
  init_paths();
10854
- import { join as join9 } from "path";
10994
+ import { join as join9, relative as relative3 } from "path";
10855
10995
  import { createHash as createHash3 } from "crypto";
10856
10996
  import { existsSync as existsSync8, readFileSync as readFileSync7, readdirSync as readdirSync5, writeFileSync as writeFileSync4 } from "fs";
10857
10997
  import yaml from "js-yaml";
@@ -10864,8 +11004,11 @@ function sha256OfFile2(path) {
10864
11004
  return "";
10865
11005
  }
10866
11006
  }
10867
- function inputsDigest2(paths) {
10868
- const combined = paths.slice().sort().map((p) => `${p}:${sha256OfFile2(p)}`).join("\n");
11007
+ function inputsDigest2(paths, cwd) {
11008
+ const combined = paths.slice().sort().map((p) => {
11009
+ const rel = relative3(cwd, p).replace(/\\/g, "/");
11010
+ return `${rel}:${sha256OfFile2(p)}`;
11011
+ }).join("\n");
10869
11012
  return createHash3("sha256").update(combined).digest("hex");
10870
11013
  }
10871
11014
  function findContractFiles2(dir, found = []) {
@@ -10893,8 +11036,8 @@ async function ensureFreshContextIndexes(cwd) {
10893
11036
  const policyPath = join9(cwd, ".cdd", "context-policy.json");
10894
11037
  const policyInputs = [policyPath].filter(existsSync8);
10895
11038
  const contractFiles = findContractFiles2(join9(cwd, "contracts"));
10896
- const wantProjectDigest = inputsDigest2(policyInputs);
10897
- const wantContractsDigest = inputsDigest2(contractFiles);
11039
+ const wantProjectDigest = inputsDigest2(policyInputs, cwd);
11040
+ const wantContractsDigest = inputsDigest2(contractFiles, cwd);
10898
11041
  const haveProjectDigest = readIndexDigest(projectMap);
10899
11042
  const haveContractsDigest = readIndexDigest(contractsIndex);
10900
11043
  const needsScan = !existsSync8(projectMap) || !existsSync8(contractsIndex) || haveProjectDigest !== wantProjectDigest || haveContractsDigest !== wantContractsDigest;
@@ -11080,9 +11223,9 @@ async function validate(opts) {
11080
11223
  var import_ajv = __toESM(require_ajv(), 1);
11081
11224
  var import_ajv_formats = __toESM(require_dist(), 1);
11082
11225
  init_logger();
11083
- import { existsSync as existsSync12, readFileSync as readFileSync9, readdirSync as readdirSync7 } from "fs";
11226
+ import { existsSync as existsSync13, readFileSync as readFileSync15, readdirSync as readdirSync7 } from "fs";
11084
11227
  import { homedir as homedir3 } from "os";
11085
- import { join as join14 } from "path";
11228
+ import { join as join16 } from "path";
11086
11229
  import yaml2 from "js-yaml";
11087
11230
  import picomatch2 from "picomatch";
11088
11231
 
@@ -11260,15 +11403,15 @@ function parseContextManifest(content) {
11260
11403
  }
11261
11404
  function extractRequiredArtifactTypes(cwd, agentName) {
11262
11405
  const candidates = [
11263
- join14(cwd, ".claude", "agents", `${agentName}.md`),
11264
- join14(homedir3(), ".claude", "agents", `${agentName}.md`)
11406
+ join16(cwd, ".claude", "agents", `${agentName}.md`),
11407
+ join16(homedir3(), ".claude", "agents", `${agentName}.md`)
11265
11408
  ];
11266
11409
  let content = null;
11267
11410
  for (const candidate of candidates) {
11268
- if (!existsSync12(candidate))
11411
+ if (!existsSync13(candidate))
11269
11412
  continue;
11270
11413
  try {
11271
- content = readFileSync9(candidate, "utf8");
11414
+ content = readFileSync15(candidate, "utf8");
11272
11415
  break;
11273
11416
  } catch {
11274
11417
  }
@@ -11305,11 +11448,11 @@ function loadContextPolicy(cwd) {
11305
11448
  unknownFilesRead: "warn-for-legacy-fail-for-new"
11306
11449
  }
11307
11450
  };
11308
- const policyPath = join14(cwd, ".cdd", "context-policy.json");
11309
- if (!existsSync12(policyPath))
11451
+ const policyPath = join16(cwd, ".cdd", "context-policy.json");
11452
+ if (!existsSync13(policyPath))
11310
11453
  return defaults;
11311
11454
  try {
11312
- const custom = JSON.parse(readFileSync9(policyPath, "utf8"));
11455
+ const custom = JSON.parse(readFileSync15(policyPath, "utf8"));
11313
11456
  return {
11314
11457
  ...defaults,
11315
11458
  ...custom,
@@ -11323,7 +11466,7 @@ function loadContextPolicy(cwd) {
11323
11466
  }
11324
11467
  function loadYamlFile(path) {
11325
11468
  try {
11326
- const raw = readFileSync9(path, "utf8");
11469
+ const raw = readFileSync15(path, "utf8");
11327
11470
  return { data: yaml2.load(raw), parseError: null };
11328
11471
  } catch (err) {
11329
11472
  return { data: null, parseError: err.message };
@@ -11354,8 +11497,8 @@ function ajvErrorsToMessages(errors, prefix, knownKeys) {
11354
11497
  return out;
11355
11498
  }
11356
11499
  function isContextGovernedChange(changeDir) {
11357
- const tasksPath = join14(changeDir, "tasks.yml");
11358
- if (!existsSync12(tasksPath))
11500
+ const tasksPath = join16(changeDir, "tasks.yml");
11501
+ if (!existsSync13(tasksPath))
11359
11502
  return false;
11360
11503
  const { data } = loadYamlFile(tasksPath);
11361
11504
  return data?.["context-governance"] === "v1";
@@ -11383,12 +11526,12 @@ function lintTasksFile(tasksPath, errors, warnings) {
11383
11526
  return data;
11384
11527
  }
11385
11528
  function resolveTier(changeDir) {
11386
- const classifPath = join14(changeDir, "change-classification.md");
11387
- const classificationPresent = existsSync12(classifPath);
11388
- const classificationText = classificationPresent ? readFileSync9(classifPath, "utf8") : "";
11529
+ const classifPath = join16(changeDir, "change-classification.md");
11530
+ const classificationPresent = existsSync13(classifPath);
11531
+ const classificationText = classificationPresent ? readFileSync15(classifPath, "utf8") : "";
11389
11532
  const classificationHasLooseMarker = classificationPresent && TIER_PATTERN.test(classificationText);
11390
- const tasksPath = join14(changeDir, "tasks.yml");
11391
- if (existsSync12(tasksPath)) {
11533
+ const tasksPath = join16(changeDir, "tasks.yml");
11534
+ if (existsSync13(tasksPath)) {
11392
11535
  const { data } = loadYamlFile(tasksPath);
11393
11536
  const t = data?.tier;
11394
11537
  if (typeof t === "number" && t >= 0 && t <= 5) {
@@ -11434,7 +11577,7 @@ function enforceTierRequirements(changeDir, agentLogDir, errors, warnings) {
11434
11577
  return;
11435
11578
  }
11436
11579
  const tier = resolution.tier;
11437
- const agentLogFiles = agentLogDir && existsSync12(agentLogDir) ? readdirSync7(agentLogDir).map((f) => f.replace(/\.ya?ml$/, "")) : [];
11580
+ const agentLogFiles = agentLogDir && existsSync13(agentLogDir) ? readdirSync7(agentLogDir).map((f) => f.replace(/\.ya?ml$/, "")) : [];
11438
11581
  if (tier <= 1) {
11439
11582
  for (const required of ["e2e-resilience-engineer", "monkey-test-engineer", "stress-soak-engineer"]) {
11440
11583
  if (!agentLogFiles.includes(required)) {
@@ -11450,7 +11593,7 @@ function enforceTierRequirements(changeDir, agentLogDir, errors, warnings) {
11450
11593
  }
11451
11594
  }
11452
11595
  if (resolution.source === "tasks-frontmatter" && resolution.classificationPresent) {
11453
- const text = readFileSync9(join14(changeDir, "change-classification.md"), "utf8");
11596
+ const text = readFileSync15(join16(changeDir, "change-classification.md"), "utf8");
11454
11597
  const structured = text.match(/^## Tier\s*\n\s*-\s*(\d)\s*$/m);
11455
11598
  const bold = text.match(/\*\*Tier:\*\*\s*Tier\s*(\d)\b/i);
11456
11599
  const classifTier = structured ? parseInt(structured[1], 10) : bold ? parseInt(bold[1], 10) : NaN;
@@ -11462,11 +11605,11 @@ function enforceTierRequirements(changeDir, agentLogDir, errors, warnings) {
11462
11605
  }
11463
11606
  }
11464
11607
  function isArchivedChange(cwd, changeId) {
11465
- const archiveRoot = join14(cwd, "specs", "archive");
11466
- if (!existsSync12(archiveRoot))
11608
+ const archiveRoot = join16(cwd, "specs", "archive");
11609
+ if (!existsSync13(archiveRoot))
11467
11610
  return false;
11468
11611
  const years = readdirSync7(archiveRoot, { withFileTypes: true }).filter((d) => d.isDirectory());
11469
- return years.some((year) => existsSync12(join14(archiveRoot, year.name, changeId)));
11612
+ return years.some((year) => existsSync13(join16(archiveRoot, year.name, changeId)));
11470
11613
  }
11471
11614
  function detectDependencyCycle(cwd, startChangeId) {
11472
11615
  const visited = /* @__PURE__ */ new Set();
@@ -11479,8 +11622,8 @@ function detectDependencyCycle(cwd, startChangeId) {
11479
11622
  return null;
11480
11623
  visited.add(id);
11481
11624
  stack.push(id);
11482
- const tasksPath = join14(cwd, "specs", "changes", id, "tasks.yml");
11483
- if (existsSync12(tasksPath)) {
11625
+ const tasksPath = join16(cwd, "specs", "changes", id, "tasks.yml");
11626
+ if (existsSync13(tasksPath)) {
11484
11627
  const { data } = loadYamlFile(tasksPath);
11485
11628
  const deps = data?.["depends-on"] ?? [];
11486
11629
  for (const dep of deps) {
@@ -11495,8 +11638,8 @@ function detectDependencyCycle(cwd, startChangeId) {
11495
11638
  return visit(startChangeId);
11496
11639
  }
11497
11640
  function validateDependencies(cwd, changeId, changeDir) {
11498
- const tasksPath = join14(changeDir, "tasks.yml");
11499
- if (!existsSync12(tasksPath))
11641
+ const tasksPath = join16(changeDir, "tasks.yml");
11642
+ if (!existsSync13(tasksPath))
11500
11643
  return [];
11501
11644
  const { data } = loadYamlFile(tasksPath);
11502
11645
  const dependencies = data?.["depends-on"] ?? [];
@@ -11510,10 +11653,10 @@ function validateDependencies(cwd, changeId, changeDir) {
11510
11653
  errors.push(`tasks.yml: change cannot depend on itself (${dep})`);
11511
11654
  continue;
11512
11655
  }
11513
- const upstreamDir = join14(cwd, "specs", "changes", dep);
11514
- if (existsSync12(upstreamDir)) {
11515
- const upstreamTasks = join14(upstreamDir, "tasks.yml");
11516
- if (!existsSync12(upstreamTasks)) {
11656
+ const upstreamDir = join16(cwd, "specs", "changes", dep);
11657
+ if (existsSync13(upstreamDir)) {
11658
+ const upstreamTasks = join16(upstreamDir, "tasks.yml");
11659
+ if (!existsSync13(upstreamTasks)) {
11517
11660
  errors.push(`dependency ${dep}: missing tasks.yml`);
11518
11661
  continue;
11519
11662
  }
@@ -11534,8 +11677,8 @@ async function gate(changeId, opts = {}) {
11534
11677
  const strict = opts.strict ?? false;
11535
11678
  const lax = opts.lax ?? false;
11536
11679
  const cwd = process.cwd();
11537
- const changeDir = join14(cwd, "specs", "changes", changeId);
11538
- if (!existsSync12(changeDir)) {
11680
+ const changeDir = join16(cwd, "specs", "changes", changeId);
11681
+ if (!existsSync13(changeDir)) {
11539
11682
  log.error(`change not found: ${changeId} (looked in ${changeDir})`);
11540
11683
  process.exit(1);
11541
11684
  }
@@ -11561,13 +11704,13 @@ async function gate(changeId, opts = {}) {
11561
11704
  }
11562
11705
  const contextPolicy = loadContextPolicy(cwd);
11563
11706
  const isNewChange = isContextGovernedChange(changeDir);
11564
- const manifestPath = join14(changeDir, "context-manifest.md");
11565
- const hasManifest = existsSync12(manifestPath);
11707
+ const manifestPath = join16(changeDir, "context-manifest.md");
11708
+ const hasManifest = existsSync13(manifestPath);
11566
11709
  let allowedPaths = [];
11567
11710
  let approvedExpansions = [];
11568
11711
  errors.push(...validateDependencies(cwd, changeId, changeDir));
11569
11712
  if (hasManifest) {
11570
- const manifest = parseContextManifest(readFileSync9(manifestPath, "utf8"));
11713
+ const manifest = parseContextManifest(readFileSync15(manifestPath, "utf8"));
11571
11714
  allowedPaths = manifest.allowedPaths;
11572
11715
  approvedExpansions = manifest.approvedExpansions;
11573
11716
  if (manifest.pendingExpansions > 0) {
@@ -11585,7 +11728,7 @@ async function gate(changeId, opts = {}) {
11585
11728
  }
11586
11729
  continue;
11587
11730
  }
11588
- if (!existsSync12(join14(changeDir, f))) {
11731
+ if (!existsSync13(join16(changeDir, f))) {
11589
11732
  errors.push(`missing required artifact: ${f}`);
11590
11733
  }
11591
11734
  }
@@ -11595,21 +11738,21 @@ async function gate(changeId, opts = {}) {
11595
11738
  continue;
11596
11739
  if (f === "tasks.yml")
11597
11740
  continue;
11598
- const content = readFileSync9(join14(changeDir, f), "utf8");
11741
+ const content = readFileSync15(join16(changeDir, f), "utf8");
11599
11742
  const minChars = MIN_CHARS[f] ?? 100;
11600
11743
  if (meaningfulChars(content) < minChars) {
11601
11744
  errors.push(`${f}: appears to be a stub (< ${minChars} meaningful chars)`);
11602
11745
  }
11603
11746
  }
11604
- const classifPath = join14(changeDir, "change-classification.md");
11747
+ const classifPath = join16(changeDir, "change-classification.md");
11605
11748
  const tierResolution = resolveTier(changeDir);
11606
- if (tierResolution.tier === null && existsSync12(classifPath) && !tierResolution.classificationHasLooseMarker) {
11749
+ if (tierResolution.tier === null && existsSync13(classifPath) && !tierResolution.classificationHasLooseMarker) {
11607
11750
  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)");
11608
11751
  }
11609
11752
  }
11610
- const tasksPath = join14(changeDir, "tasks.yml");
11753
+ const tasksPath = join16(changeDir, "tasks.yml");
11611
11754
  let tasksData = null;
11612
- if (existsSync12(tasksPath)) {
11755
+ if (existsSync13(tasksPath)) {
11613
11756
  tasksData = lintTasksFile(tasksPath, errors, warnings);
11614
11757
  }
11615
11758
  if (tasksData) {
@@ -11623,11 +11766,11 @@ async function gate(changeId, opts = {}) {
11623
11766
  }
11624
11767
  }
11625
11768
  }
11626
- const agentLogDir = join14(changeDir, "agent-log");
11627
- if (existsSync12(agentLogDir)) {
11769
+ const agentLogDir = join16(changeDir, "agent-log");
11770
+ if (existsSync13(agentLogDir)) {
11628
11771
  const logFiles = readdirSync7(agentLogDir).filter((f) => f.endsWith(".yml"));
11629
11772
  for (const f of logFiles) {
11630
- const fullPath = join14(agentLogDir, f);
11773
+ const fullPath = join16(agentLogDir, f);
11631
11774
  const { data, parseError } = loadYamlFile(fullPath);
11632
11775
  if (parseError) {
11633
11776
  errors.push(`agent-log/${f}: invalid YAML: ${parseError}`);
@@ -11695,9 +11838,9 @@ async function gate(changeId, opts = {}) {
11695
11838
  errors.push(`agent-log/${f}: read unauthorized path -> ${p} (not in allowed paths or approved expansions)`);
11696
11839
  }
11697
11840
  }
11698
- const runtimeLog = join14(cwd, ".cdd", "runtime", `${changeId}-files-read.jsonl`);
11699
- if (existsSync12(runtimeLog)) {
11700
- const runtimePaths = readFileSync9(runtimeLog, "utf8").split("\n").filter(Boolean).map((line) => {
11841
+ const runtimeLog = join16(cwd, ".cdd", "runtime", `${changeId}-files-read.jsonl`);
11842
+ if (existsSync13(runtimeLog)) {
11843
+ const runtimePaths = readFileSync15(runtimeLog, "utf8").split("\n").filter(Boolean).map((line) => {
11701
11844
  try {
11702
11845
  return JSON.parse(line).path;
11703
11846
  } catch {
@@ -11733,8 +11876,8 @@ async function gate(changeId, opts = {}) {
11733
11876
  continue;
11734
11877
  const pathPart = pointer.split(":")[0];
11735
11878
  if (pathPart.includes("/") && !pointer.startsWith("http")) {
11736
- const abs = join14(cwd, pathPart);
11737
- if (!existsSync12(abs)) {
11879
+ const abs = join16(cwd, pathPart);
11880
+ if (!existsSync13(abs)) {
11738
11881
  errors.push(`agent-log/${f}: artifact pointer not found: ${pathPart}`);
11739
11882
  }
11740
11883
  }
@@ -11783,26 +11926,26 @@ async function gate(changeId, opts = {}) {
11783
11926
  // src/commands/install-hooks.ts
11784
11927
  init_paths();
11785
11928
  init_logger();
11786
- import { existsSync as existsSync13, readFileSync as readFileSync10, writeFileSync as writeFileSync5, chmodSync as chmodSync2, mkdirSync as mkdirSync5 } from "fs";
11787
- import { join as join15 } from "path";
11929
+ import { existsSync as existsSync14, readFileSync as readFileSync16, writeFileSync as writeFileSync7, chmodSync as chmodSync2, mkdirSync as mkdirSync6 } from "fs";
11930
+ import { join as join17 } from "path";
11788
11931
  var START_MARKER2 = "# cdd-kit-managed-block-start";
11789
11932
  var END_MARKER2 = "# cdd-kit-managed-block-end";
11790
11933
  async function installHooks() {
11791
11934
  const cwd = process.cwd();
11792
- const gitDir = join15(cwd, ".git");
11793
- if (!existsSync13(gitDir)) {
11935
+ const gitDir = join17(cwd, ".git");
11936
+ if (!existsSync14(gitDir)) {
11794
11937
  log.error("not a git repository (no .git/ found in cwd)");
11795
11938
  process.exit(1);
11796
11939
  }
11797
- const hooksDir = join15(gitDir, "hooks");
11798
- mkdirSync5(hooksDir, { recursive: true });
11799
- const dest = join15(hooksDir, "pre-commit");
11800
- const ourHook = readFileSync10(join15(ASSET.hooks, "pre-commit"), "utf8");
11940
+ const hooksDir = join17(gitDir, "hooks");
11941
+ mkdirSync6(hooksDir, { recursive: true });
11942
+ const dest = join17(hooksDir, "pre-commit");
11943
+ const ourHook = readFileSync16(join17(ASSET.hooks, "pre-commit"), "utf8");
11801
11944
  let final;
11802
- if (!existsSync13(dest)) {
11945
+ if (!existsSync14(dest)) {
11803
11946
  final = ourHook;
11804
11947
  } else {
11805
- const existing = readFileSync10(dest, "utf8");
11948
+ const existing = readFileSync16(dest, "utf8");
11806
11949
  const startIdx = existing.indexOf(START_MARKER2);
11807
11950
  const endIdx = existing.indexOf(END_MARKER2);
11808
11951
  if (startIdx >= 0 && endIdx > startIdx) {
@@ -11826,7 +11969,7 @@ async function installHooks() {
11826
11969
  }
11827
11970
  }
11828
11971
  }
11829
- writeFileSync5(dest, final, "utf8");
11972
+ writeFileSync7(dest, final, "utf8");
11830
11973
  try {
11831
11974
  chmodSync2(dest, 493);
11832
11975
  } catch {
@@ -11837,7 +11980,7 @@ async function installHooks() {
11837
11980
 
11838
11981
  // src/cli/index.ts
11839
11982
  var __dirname2 = dirname7(fileURLToPath3(import.meta.url));
11840
- var pkg = JSON.parse(readFileSync25(join27(__dirname2, "..", "..", "package.json"), "utf8"));
11983
+ var pkg = JSON.parse(readFileSync26(join27(__dirname2, "..", "..", "package.json"), "utf8"));
11841
11984
  var program = new Command();
11842
11985
  program.name("cdd-kit").description("Contract-Driven Delivery Kit CLI").version(pkg.version);
11843
11986
  program.command("init").description(